Skip to content

Commit 9af2570

Browse files
committed
Retry 429, 500, and connection error API requests to the trigger.dev server
1 parent a946797 commit 9af2570

File tree

4 files changed

+103
-17
lines changed

4 files changed

+103
-17
lines changed

.changeset/khaki-apricots-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Retry 429, 500, and connection error API requests to the trigger.dev server

packages/core/src/v3/apiClient/index.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { context, propagation } from "@opentelemetry/api";
2-
import { zodfetch } from "../../zodfetch";
2+
import { ZodFetchOptions, zodfetch } from "../../zodfetch";
33
import { taskContextManager } from "../tasks/taskContextManager";
44
import { SafeAsyncLocalStorage } from "../utils/safeAsyncLocalStorage";
55
import { getEnvVar } from "../utils/getEnv";
@@ -15,6 +15,16 @@ export type TriggerOptions = {
1515
spanParentAsLink?: boolean;
1616
};
1717

18+
const zodFetchOptions: ZodFetchOptions = {
19+
retry: {
20+
maxAttempts: 5,
21+
minTimeoutInMs: 1000,
22+
maxTimeoutInMs: 30_000,
23+
factor: 2,
24+
randomize: false,
25+
},
26+
};
27+
1828
/**
1929
* Trigger.dev v3 API client
2030
*/
@@ -29,19 +39,29 @@ export class ApiClient {
2939
}
3040

3141
triggerTask(taskId: string, body: TriggerTaskRequestBody, options?: TriggerOptions) {
32-
return zodfetch(TriggerTaskResponse, `${this.baseUrl}/api/v1/tasks/${taskId}/trigger`, {
33-
method: "POST",
34-
headers: this.#getHeaders(options?.spanParentAsLink ?? false),
35-
body: JSON.stringify(body),
36-
});
42+
return zodfetch(
43+
TriggerTaskResponse,
44+
`${this.baseUrl}/api/v1/tasks/${taskId}/trigger`,
45+
{
46+
method: "POST",
47+
headers: this.#getHeaders(options?.spanParentAsLink ?? false),
48+
body: JSON.stringify(body),
49+
},
50+
zodFetchOptions
51+
);
3752
}
3853

3954
batchTriggerTask(taskId: string, body: BatchTriggerTaskRequestBody, options?: TriggerOptions) {
40-
return zodfetch(BatchTriggerTaskResponse, `${this.baseUrl}/api/v1/tasks/${taskId}/batch`, {
41-
method: "POST",
42-
headers: this.#getHeaders(options?.spanParentAsLink ?? false),
43-
body: JSON.stringify(body),
44-
});
55+
return zodfetch(
56+
BatchTriggerTaskResponse,
57+
`${this.baseUrl}/api/v1/tasks/${taskId}/batch`,
58+
{
59+
method: "POST",
60+
headers: this.#getHeaders(options?.spanParentAsLink ?? false),
61+
body: JSON.stringify(body),
62+
},
63+
zodFetchOptions
64+
);
4565
}
4666

4767
createUploadPayloadUrl(filename: string) {
@@ -51,7 +71,8 @@ export class ApiClient {
5171
{
5272
method: "PUT",
5373
headers: this.#getHeaders(false),
54-
}
74+
},
75+
zodFetchOptions
5576
);
5677
}
5778

@@ -62,7 +83,8 @@ export class ApiClient {
6283
{
6384
method: "GET",
6485
headers: this.#getHeaders(false),
65-
}
86+
},
87+
zodFetchOptions
6688
);
6789
}
6890

packages/core/src/zodfetch.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
import { z } from "zod";
2-
import { context, propagation } from "@opentelemetry/api";
2+
import { RetryOptions, calculateNextRetryDelay, defaultRetryOptions } from "./v3";
33

4-
type ApiResult<TSuccessResult> =
4+
export type ApiResult<TSuccessResult> =
55
| { ok: true; data: TSuccessResult }
66
| {
77
ok: false;
88
error: string;
99
};
1010

11+
export type ZodFetchOptions = {
12+
retry?: RetryOptions;
13+
};
14+
1115
export async function zodfetch<TResponseBody extends any>(
1216
schema: z.Schema<TResponseBody>,
1317
url: string,
14-
requestInit?: RequestInit
18+
requestInit?: RequestInit,
19+
options?: ZodFetchOptions
20+
): Promise<ApiResult<TResponseBody>> {
21+
return await _doZodFetch(schema, url, requestInit, options);
22+
}
23+
24+
async function _doZodFetch<TResponseBody extends any>(
25+
schema: z.Schema<TResponseBody>,
26+
url: string,
27+
requestInit?: RequestInit,
28+
options?: ZodFetchOptions,
29+
attempt = 1
1530
): Promise<ApiResult<TResponseBody>> {
1631
try {
1732
const response = await fetch(url, requestInit);
@@ -23,7 +38,7 @@ export async function zodfetch<TResponseBody extends any>(
2338
};
2439
}
2540

26-
if (response.status >= 400 && response.status < 500) {
41+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2742
const body = await response.json();
2843
if (!body.error) {
2944
return { ok: false, error: "Something went wrong" };
@@ -32,6 +47,31 @@ export async function zodfetch<TResponseBody extends any>(
3247
return { ok: false, error: body.error };
3348
}
3449

50+
// Retryable errors
51+
if (response.status === 429 || response.status >= 500) {
52+
if (!options?.retry) {
53+
return {
54+
ok: false,
55+
error: `Failed to fetch ${url}, got status code ${response.status}`,
56+
};
57+
}
58+
59+
const retry = { ...defaultRetryOptions, ...options.retry };
60+
61+
if (attempt > retry.maxAttempts) {
62+
return {
63+
ok: false,
64+
error: `Failed to fetch ${url}, got status code ${response.status}`,
65+
};
66+
}
67+
68+
const delay = calculateNextRetryDelay(retry, attempt);
69+
70+
await new Promise((resolve) => setTimeout(resolve, delay));
71+
72+
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
73+
}
74+
3575
if (response.status !== 200) {
3676
return {
3777
ok: false,
@@ -55,6 +95,23 @@ export async function zodfetch<TResponseBody extends any>(
5595

5696
return { ok: false, error: parsedResult.error.message };
5797
} catch (error) {
98+
if (options?.retry) {
99+
const retry = { ...defaultRetryOptions, ...options.retry };
100+
101+
if (attempt > retry.maxAttempts) {
102+
return {
103+
ok: false,
104+
error: error instanceof Error ? error.message : JSON.stringify(error),
105+
};
106+
}
107+
108+
const delay = calculateNextRetryDelay(retry, attempt);
109+
110+
await new Promise((resolve) => setTimeout(resolve, delay));
111+
112+
return await _doZodFetch(schema, url, requestInit, options, attempt + 1);
113+
}
114+
58115
return {
59116
ok: false,
60117
error: error instanceof Error ? error.message : JSON.stringify(error),

references/v3-catalog/src/trigger/concurrency.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export const testConcurrency = task({
2121
run: async ({ count = 10, delay = 5000 }: { count: number; delay: number }) => {
2222
logger.info(`Running ${count} tasks`);
2323

24+
await new Promise((resolve) => setTimeout(resolve, 3000));
25+
2426
await testConcurrencyChild.batchTrigger({
2527
items: Array.from({ length: count }).map((_, index) => ({
2628
payload: {

0 commit comments

Comments
 (0)