Skip to content

Commit 134d61d

Browse files
authored
Fix regression in retryOnAPIFailure preventing any requests from being retried (#7895)
* Fix regression in retryOnAPIFailure preventing any requests from being retried Also fixes a regression in pipelines that prevented 401 errors from being retried when waiting for an API token to become active. * fixup! Retry TypeError * fixup! Don't retry other errors
1 parent 13ab591 commit 134d61d

File tree

4 files changed

+123
-9
lines changed

4 files changed

+123
-9
lines changed

.changeset/neat-needles-brush.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Fix regression in retryOnAPIFailure preventing any requests from being retried
6+
7+
Also fixes a regression in pipelines that prevented 401 errors from being retried when waiting for an API token to become active.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { APIError } from "../../parse";
2+
import { retryOnAPIFailure } from "../../utils/retry";
3+
4+
describe("retryOnAPIFailure", () => {
5+
it("should retry 5xx errors and succeed if the 3rd try succeeds", async () => {
6+
let attempts = 0;
7+
8+
await retryOnAPIFailure(() => {
9+
attempts++;
10+
if (attempts < 3) {
11+
throw new APIError({ status: 500, text: "500 error" });
12+
}
13+
});
14+
expect(attempts).toBe(3);
15+
});
16+
17+
it("should throw 5xx error after all retries fail", async () => {
18+
let attempts = 0;
19+
20+
await expect(() =>
21+
retryOnAPIFailure(() => {
22+
attempts++;
23+
throw new APIError({ status: 500, text: "500 error" });
24+
})
25+
).rejects.toMatchInlineSnapshot(`[APIError: 500 error]`);
26+
expect(attempts).toBe(3);
27+
});
28+
29+
it("should not retry non-5xx errors", async () => {
30+
let attempts = 0;
31+
32+
await expect(() =>
33+
retryOnAPIFailure(() => {
34+
attempts++;
35+
throw new APIError({ status: 401, text: "401 error" });
36+
})
37+
).rejects.toMatchInlineSnapshot(`[APIError: 401 error]`);
38+
expect(attempts).toBe(1);
39+
});
40+
41+
it("should retry TypeError", async () => {
42+
let attempts = 0;
43+
44+
await expect(() =>
45+
retryOnAPIFailure(() => {
46+
attempts++;
47+
throw new TypeError("type error");
48+
})
49+
).rejects.toMatchInlineSnapshot(`[TypeError: type error]`);
50+
expect(attempts).toBe(3);
51+
});
52+
53+
it("should not retry other errors", async () => {
54+
let attempts = 0;
55+
56+
await expect(() =>
57+
retryOnAPIFailure(() => {
58+
attempts++;
59+
throw new Error("some error");
60+
})
61+
).rejects.toMatchInlineSnapshot(`[Error: some error]`);
62+
expect(attempts).toBe(1);
63+
});
64+
65+
it("should retry custom APIError implementation with non-5xx error", async () => {
66+
let checkedCustomIsRetryable = false;
67+
class CustomAPIError extends APIError {
68+
isRetryable(): boolean {
69+
checkedCustomIsRetryable = true;
70+
return true;
71+
}
72+
}
73+
74+
let attempts = 0;
75+
76+
await expect(() =>
77+
retryOnAPIFailure(() => {
78+
attempts++;
79+
throw new CustomAPIError({ status: 401, text: "401 error" });
80+
})
81+
).rejects.toMatchInlineSnapshot(`[CustomAPIError: 401 error]`);
82+
expect(attempts).toBe(3);
83+
expect(checkedCustomIsRetryable).toBe(true);
84+
});
85+
});

packages/wrangler/src/pipelines/index.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,21 @@ async function authorizeR2Bucket(
6565
!__testSkipDelaysFlag &&
6666
(await retryOnAPIFailure(
6767
async () => {
68-
await r2.send(
69-
new HeadBucketCommand({
70-
Bucket: bucketName,
71-
})
72-
);
68+
try {
69+
await r2.send(
70+
new HeadBucketCommand({
71+
Bucket: bucketName,
72+
})
73+
);
74+
} catch (err) {
75+
if (err instanceof Error && err.name === "401") {
76+
throw new AuthAPIError({
77+
status: 401,
78+
text: "R2 HeadBucket request failed with status: 401",
79+
});
80+
}
81+
throw err;
82+
}
7383
},
7484
1000,
7585
10
@@ -78,6 +88,17 @@ async function authorizeR2Bucket(
7888
return serviceToken;
7989
}
8090

91+
/**
92+
* AuthAPIError always retries errors so that
93+
* we always retry auth errors while waiting for an
94+
* API token to propegate and start working.
95+
*/
96+
class AuthAPIError extends APIError {
97+
override isRetryable(): boolean {
98+
return true;
99+
}
100+
}
101+
81102
function getAccountR2Endpoint(accountId: string) {
82103
return `https://${accountId}.r2.cloudflarestorage.com`;
83104
}

packages/wrangler/src/utils/retry.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export async function retryOnAPIFailure<T>(
2121
try {
2222
return await action();
2323
} catch (err) {
24-
if (
25-
(err instanceof APIError && !err.isRetryable()) ||
26-
!(err instanceof TypeError)
27-
) {
24+
if (err instanceof APIError) {
25+
if (!err.isRetryable()) {
26+
throw err;
27+
}
28+
} else if (!(err instanceof TypeError)) {
2829
throw err;
2930
}
3031

0 commit comments

Comments
 (0)