Skip to content

Commit 28494f4

Browse files
fix NonRetryableError thrown with an empty error message not stopping workflow retries locally (#10219)
1 parent eb712ef commit 28494f4

File tree

5 files changed

+143
-1
lines changed

5 files changed

+143
-1
lines changed

.changeset/shy-coats-drum.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudflare/workflows-shared": patch
3+
"wrangler": patch
4+
---
5+
6+
fix `NonRetryableError` thrown with an empty error message not stopping workflow retries locally

fixtures/workflow/src/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
WorkflowEvent,
66
WorkflowStep,
77
} from "cloudflare:workers";
8+
import { NonRetryableError } from "cloudflare:workflows";
89

910
type Params = {
1011
name: string;
@@ -56,14 +57,47 @@ export class Demo2 extends WorkflowEntrypoint<{}, Params> {
5657
}
5758
}
5859

60+
export class Demo3 extends WorkflowEntrypoint<{}, Params> {
61+
async run(
62+
event: WorkflowEvent<Params & { doRetry: boolean; errorMessage: string }>,
63+
step: WorkflowStep
64+
) {
65+
let runs = -1;
66+
try {
67+
await step.do(
68+
"First (and only step) step",
69+
{
70+
retries: {
71+
limit: 3,
72+
delay: 100,
73+
},
74+
},
75+
async function () {
76+
runs++;
77+
if (event.payload.doRetry) {
78+
throw new Error(event.payload.errorMessage);
79+
} else {
80+
throw new NonRetryableError(event.payload.errorMessage);
81+
}
82+
}
83+
);
84+
} catch {}
85+
86+
return `The step was retried ${runs} time${runs === 1 ? "" : "s"}`;
87+
}
88+
}
89+
5990
type Env = {
6091
WORKFLOW: Workflow;
6192
WORKFLOW2: Workflow;
93+
WORKFLOW3: Workflow<{ doRetry: boolean; errorMessage: string }>;
6294
};
6395
export default class extends WorkerEntrypoint<Env> {
6496
async fetch(req: Request) {
6597
const url = new URL(req.url);
6698
const id = url.searchParams.get("workflowName");
99+
const doRetry = url.searchParams.get("doRetry");
100+
const errorMessage = url.searchParams.get("errorMessage");
67101

68102
if (url.pathname === "/favicon.ico") {
69103
return new Response(null, { status: 404 });
@@ -89,6 +123,18 @@ export default class extends WorkerEntrypoint<Env> {
89123
} else {
90124
handle = await this.env.WORKFLOW2.create({ id });
91125
}
126+
} else if (url.pathname === "/createDemo3") {
127+
if (id === null) {
128+
handle = await this.env.WORKFLOW3.create();
129+
} else {
130+
handle = await this.env.WORKFLOW3.create({
131+
id,
132+
params: {
133+
doRetry: doRetry === "false" ? false : true,
134+
errorMessage: errorMessage ?? "",
135+
},
136+
});
137+
}
92138
} else if (url.pathname === "/sendEvent") {
93139
handle = await this.env.WORKFLOW2.get(id);
94140

@@ -98,6 +144,8 @@ export default class extends WorkerEntrypoint<Env> {
98144
});
99145
} else if (url.pathname === "/get2") {
100146
handle = await this.env.WORKFLOW2.get(id);
147+
} else if (url.pathname === "/get3") {
148+
handle = await this.env.WORKFLOW3.get(id);
101149
} else {
102150
handle = await this.env.WORKFLOW.get(id);
103151
}

fixtures/workflow/tests/index.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from "crypto";
12
import { rm } from "fs/promises";
23
import { resolve } from "path";
34
import { fetch } from "undici";
@@ -139,6 +140,88 @@ describe("Workflows", () => {
139140
]);
140141
});
141142

143+
describe("retrying a step", () => {
144+
test("should retry a step if a generic Error (with a generic error message) is thrown", async ({
145+
expect,
146+
}) => {
147+
const name = randomUUID();
148+
await fetchJson(
149+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=generic_error_message`
150+
);
151+
152+
await vi.waitFor(
153+
async () => {
154+
const result = await fetchJson(
155+
`http://${ip}:${port}/get3?workflowName=${name}`
156+
);
157+
158+
expect(result["output"]).toEqual("The step was retried 3 times");
159+
},
160+
{ timeout: 1500 }
161+
);
162+
});
163+
164+
test("should retry a step if a generic Error (with an empty error message) is thrown", async ({
165+
expect,
166+
}) => {
167+
const name = randomUUID();
168+
await fetchJson(
169+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=`
170+
);
171+
172+
await vi.waitFor(
173+
async () => {
174+
const result = await fetchJson(
175+
`http://${ip}:${port}/get3?workflowName=${name}`
176+
);
177+
178+
expect(result["output"]).toEqual("The step was retried 3 times");
179+
},
180+
{ timeout: 1500 }
181+
);
182+
});
183+
184+
test("should not retry a step if a NonRetryableError (with a generic error message) is thrown", async ({
185+
expect,
186+
}) => {
187+
const name = randomUUID();
188+
await fetchJson(
189+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=false&errorMessage=generic_error_message"`
190+
);
191+
192+
await vi.waitFor(
193+
async () => {
194+
const result = await fetchJson(
195+
`http://${ip}:${port}/get3?workflowName=${name}`
196+
);
197+
198+
expect(result["output"]).toEqual("The step was retried 0 times");
199+
},
200+
{ timeout: 1500 }
201+
);
202+
});
203+
204+
test("should not retry a step if a NonRetryableError (with an empty error message) is thrown", async ({
205+
expect,
206+
}) => {
207+
const name = randomUUID();
208+
await fetchJson(
209+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=false&errorMessage=`
210+
);
211+
212+
await vi.waitFor(
213+
async () => {
214+
const result = await fetchJson(
215+
`http://${ip}:${port}/get3?workflowName=${name}`
216+
);
217+
218+
expect(result["output"]).toEqual("The step was retried 0 times");
219+
},
220+
{ timeout: 1500 }
221+
);
222+
});
223+
});
224+
142225
test("waitForEvent should work", async ({ expect }) => {
143226
await fetchJson(`http://${ip}:${port}/createDemo2?workflowName=something`);
144227

fixtures/workflow/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@
1313
"name": "my-workflow2",
1414
"class_name": "Demo2",
1515
},
16+
{
17+
"binding": "WORKFLOW3",
18+
"name": "my-workflow3",
19+
"class_name": "Demo3",
20+
},
1621
],
1722
}

packages/workflows-shared/src/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ export class Context extends RpcTarget {
350350
if (
351351
e instanceof Error &&
352352
(error.name === "NonRetryableError" ||
353-
error.message.startsWith("NonRetryableError:"))
353+
error.message.startsWith("NonRetryableError"))
354354
) {
355355
this.#engine.writeLog(
356356
InstanceEvent.ATTEMPT_FAILURE,

0 commit comments

Comments
 (0)