Skip to content

Commit 77a4364

Browse files
fix NonRetryableError thrown with an empty error message not stopping workflow retries locally (#10228)
1 parent bb14da5 commit 77a4364

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
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
@@ -4,6 +4,7 @@ import {
44
WorkflowEvent,
55
WorkflowStep,
66
} from "cloudflare:workers";
7+
import { NonRetryableError } from "cloudflare:workflows";
78

89
type Params = {
910
name: string;
@@ -31,13 +32,46 @@ export class Demo extends WorkflowEntrypoint<{}, Params> {
3132
}
3233
}
3334

35+
export class Demo3 extends WorkflowEntrypoint<{}, Params> {
36+
async run(
37+
event: WorkflowEvent<Params & { doRetry: boolean; errorMessage: string }>,
38+
step: WorkflowStep
39+
) {
40+
let runs = -1;
41+
try {
42+
await step.do(
43+
"First (and only step) step",
44+
{
45+
retries: {
46+
limit: 3,
47+
delay: 100,
48+
},
49+
},
50+
async function () {
51+
runs++;
52+
if (event.payload.doRetry) {
53+
throw new Error(event.payload.errorMessage);
54+
} else {
55+
throw new NonRetryableError(event.payload.errorMessage);
56+
}
57+
}
58+
);
59+
} catch {}
60+
61+
return `The step was retried ${runs} time${runs === 1 ? "" : "s"}`;
62+
}
63+
}
64+
3465
type Env = {
3566
WORKFLOW: Workflow;
67+
WORKFLOW3: Workflow<{ doRetry: boolean; errorMessage: string }>;
3668
};
3769
export default class extends WorkerEntrypoint<Env> {
3870
async fetch(req: Request) {
3971
const url = new URL(req.url);
4072
const id = url.searchParams.get("workflowName");
73+
const doRetry = url.searchParams.get("doRetry");
74+
const errorMessage = url.searchParams.get("errorMessage");
4175

4276
if (url.pathname === "/favicon.ico") {
4377
return new Response(null, { status: 404 });
@@ -50,6 +84,20 @@ export default class extends WorkerEntrypoint<Env> {
5084
} else {
5185
handle = await this.env.WORKFLOW.create({ id });
5286
}
87+
} else if (url.pathname === "/createDemo3") {
88+
if (id === null) {
89+
handle = await this.env.WORKFLOW3.create();
90+
} else {
91+
handle = await this.env.WORKFLOW3.create({
92+
id,
93+
params: {
94+
doRetry: doRetry === "false" ? false : true,
95+
errorMessage: errorMessage ?? "",
96+
},
97+
});
98+
}
99+
} else if (url.pathname === "/get3") {
100+
handle = await this.env.WORKFLOW3.get(id);
53101
} else {
54102
handle = await this.env.WORKFLOW.get(id);
55103
}

fixtures/workflow/tests/index.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { randomUUID } from "crypto";
12
import { rm } from "fs/promises";
23
import { resolve } from "path";
34
import { fetch } from "undici";
4-
import { afterAll, beforeAll, describe, it, vi } from "vitest";
5+
import { afterAll, beforeAll, describe, it, test, vi } from "vitest";
56
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
67

78
describe("Workflows", () => {
@@ -98,4 +99,86 @@ describe("Workflows", () => {
9899
name: "Error",
99100
});
100101
});
102+
103+
describe("retrying a step", () => {
104+
test("should retry a step if a generic Error (with a generic error message) is thrown", async ({
105+
expect,
106+
}) => {
107+
const name = randomUUID();
108+
await fetchJson(
109+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=generic_error_message`
110+
);
111+
112+
await vi.waitFor(
113+
async () => {
114+
const result = await fetchJson(
115+
`http://${ip}:${port}/get3?workflowName=${name}`
116+
);
117+
118+
expect(result["output"]).toEqual("The step was retried 3 times");
119+
},
120+
{ timeout: 1500 }
121+
);
122+
});
123+
124+
test("should retry a step if a generic Error (with an empty error message) is thrown", async ({
125+
expect,
126+
}) => {
127+
const name = randomUUID();
128+
await fetchJson(
129+
`http://${ip}:${port}/createDemo3?workflowName=${name}&doRetry=true&errorMessage=`
130+
);
131+
132+
await vi.waitFor(
133+
async () => {
134+
const result = await fetchJson(
135+
`http://${ip}:${port}/get3?workflowName=${name}`
136+
);
137+
138+
expect(result["output"]).toEqual("The step was retried 3 times");
139+
},
140+
{ timeout: 1500 }
141+
);
142+
});
143+
144+
test("should not retry a step if a NonRetryableError (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=false&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 0 times");
159+
},
160+
{ timeout: 1500 }
161+
);
162+
});
163+
164+
test("should not retry a step if a NonRetryableError (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=false&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 0 times");
179+
},
180+
{ timeout: 1500 }
181+
);
182+
});
183+
});
101184
});

fixtures/workflow/wrangler.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,10 @@
88
"name": "my-workflow",
99
"class_name": "Demo",
1010
},
11+
{
12+
"binding": "WORKFLOW3",
13+
"name": "my-workflow3",
14+
"class_name": "Demo3",
15+
},
1116
],
1217
}

packages/workflows-shared/src/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export class Context extends RpcTarget {
343343
if (
344344
e instanceof Error &&
345345
(error.name === "NonRetryableError" ||
346-
error.message.startsWith("NonRetryableError:"))
346+
error.message.startsWith("NonRetryableError"))
347347
) {
348348
this.#engine.writeLog(
349349
InstanceEvent.ATTEMPT_FAILURE,

0 commit comments

Comments
 (0)