-
Notifications
You must be signed in to change notification settings - Fork 684
Expand file tree
/
Copy pathWebhookTrigger.ts
More file actions
123 lines (106 loc) · 3.45 KB
/
Copy pathWebhookTrigger.ts
File metadata and controls
123 lines (106 loc) · 3.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import pRetry, { AbortError } from 'p-retry';
import { SlackWebhookError, WebhookTriggerHTTPError, WebhookTriggerRequestError } from './errors';
import type { FetchFunction, FetchResponse } from './IncomingWebhook';
import { getUserAgent } from './instrument';
import type { RetryOptions } from './retry-policies';
/**
* A client for Slack's Workflow Builder webhook triggers
* @see {@link https://slack.com/help/articles/360041352714-Build-a-workflow--Create-a-workflow-that-starts-outside-of-Slack}
*/
export class WebhookTrigger {
/**
* The webhook trigger URL
*/
private url: string;
/**
* The fetch function used for HTTP requests
*/
private fetchFn: FetchFunction;
/**
* Request timeout in milliseconds
*/
private timeout: number;
/**
* Default headers sent with every request
*/
private headers: Record<string, string>;
/**
* Retry policy applied to each send. Defaults to no retries.
*/
private retryConfig: RetryOptions;
public constructor(
url: string,
defaults: WebhookTriggerDefaultArguments = {
timeout: 0,
},
) {
if (!url) {
throw new Error('Webhook trigger URL is required');
}
this.url = url;
this.fetchFn = defaults.fetch ?? globalThis.fetch;
this.timeout = defaults.timeout ?? 0;
this.retryConfig = defaults.retryConfig ?? { retries: 0 };
this.headers = {
'User-Agent': getUserAgent(),
};
}
/**
* Send a payload to the webhook trigger
* @param payload - arbitrary key-value data to send to the trigger
*/
public async send(payload: WebhookTriggerSendArguments = {}): Promise<WebhookTriggerResult> {
return pRetry(async () => {
const signal = this.timeout > 0 ? AbortSignal.timeout(this.timeout) : undefined;
try {
const response = await this.fetchFn(this.url, {
method: 'POST',
headers: {
...this.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
redirect: 'error',
signal,
});
if (!response.ok) {
const body = await response.text();
const httpError = new WebhookTriggerHTTPError(response.status, response.statusText, body);
// Only server errors (5xx) are transient; client errors (4xx), including rate limits (429), fail immediately.
throw response.status >= 500 ? httpError : new AbortError(httpError);
}
return await this.buildResult(response);
} catch (error) {
// Non-retryable signals (AbortError) and already-wrapped errors pass through untouched.
if (error instanceof AbortError || error instanceof SlackWebhookError) {
throw error;
}
// No response received (network/timeout): retryable.
throw new WebhookTriggerRequestError(error instanceof Error ? error : new Error(String(error)));
}
}, this.retryConfig);
}
private async buildResult(response: FetchResponse): Promise<WebhookTriggerResult> {
const text = await response.text();
try {
return text ? (JSON.parse(text) as WebhookTriggerResult) : { ok: true };
} catch {
return { ok: true };
}
}
}
/*
* Exported types
*/
export interface WebhookTriggerDefaultArguments {
fetch?: FetchFunction;
timeout?: number;
retryConfig?: RetryOptions;
}
export interface WebhookTriggerSendArguments {
[key: string]: string;
}
export interface WebhookTriggerResult {
ok: boolean;
error?: string;
}