Skip to content

Commit 0adccc7

Browse files
authored
fix: Retry deployment errors in wrangler pages publish (#3758)
Occasionally, creating a deployment can fail due to internal errors in the POST /deployments API call. Rather than failing the deployment, this will retry first.
1 parent 40de26b commit 0adccc7

File tree

4 files changed

+203
-8
lines changed

4 files changed

+203
-8
lines changed

.changeset/hip-rules-cross.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix: Retry deployment errors in wrangler pages publish
6+
7+
This will improve reliability when deploying to Cloudflare Pages

packages/wrangler/src/__tests__/pages/deploy.test.ts

+164-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ describe("deployment create", () => {
231231
success: false,
232232
errors: [
233233
{
234-
code: 800000,
234+
code: 8000000,
235235
message: "Something exploded, please retry",
236236
},
237237
],
@@ -298,6 +298,169 @@ describe("deployment create", () => {
298298

299299
await runWrangler("pages deploy . --project-name=foo");
300300

301+
// Should be 2 attempts to upload
302+
expect(requests.length).toBe(2);
303+
304+
expect(normalizeProgressSteps(std.out)).toMatchInlineSnapshot(`
305+
"✨ Success! Uploaded 1 files (TIMINGS)
306+
307+
✨ Deployment complete! Take a peek over at https://abcxyz.foo.pages.dev/"
308+
`);
309+
});
310+
311+
it("should retry POST /deployments", async () => {
312+
writeFileSync("logo.txt", "foobar");
313+
314+
mockGetUploadTokenRequest(
315+
"<<funfetti-auth-jwt>>",
316+
"some-account-id",
317+
"foo"
318+
);
319+
320+
// Accumulate multiple requests then assert afterwards
321+
const requests: RestRequest[] = [];
322+
msw.use(
323+
rest.post("*/pages/assets/check-missing", async (req, res, ctx) => {
324+
const body = await req.json();
325+
326+
expect(req.headers.get("Authorization")).toBe(
327+
"Bearer <<funfetti-auth-jwt>>"
328+
);
329+
expect(body).toMatchObject({
330+
hashes: ["1a98fb08af91aca4a7df1764a2c4ddb0"],
331+
});
332+
333+
return res.once(
334+
ctx.status(200),
335+
ctx.json({
336+
success: true,
337+
errors: [],
338+
messages: [],
339+
result: body.hashes,
340+
})
341+
);
342+
}),
343+
rest.post("*/pages/assets/upload", async (req, res, ctx) => {
344+
expect(req.headers.get("Authorization")).toBe(
345+
"Bearer <<funfetti-auth-jwt>>"
346+
);
347+
expect(await req.json()).toMatchObject([
348+
{
349+
key: "1a98fb08af91aca4a7df1764a2c4ddb0",
350+
value: Buffer.from("foobar").toString("base64"),
351+
metadata: {
352+
contentType: "text/plain",
353+
},
354+
base64: true,
355+
},
356+
]);
357+
358+
return res(
359+
ctx.status(200),
360+
ctx.json({
361+
success: true,
362+
errors: [],
363+
messages: [],
364+
result: null,
365+
})
366+
);
367+
}),
368+
rest.post(
369+
"*/accounts/:accountId/pages/projects/foo/deployments",
370+
async (req, res, ctx) => {
371+
requests.push(req);
372+
expect(req.params.accountId).toEqual("some-account-id");
373+
expect(await (req as RestRequestWithFormData).formData())
374+
.toMatchInlineSnapshot(`
375+
FormData {
376+
Symbol(state): Array [
377+
Object {
378+
"name": "manifest",
379+
"value": "{\\"/logo.txt\\":\\"1a98fb08af91aca4a7df1764a2c4ddb0\\"}",
380+
},
381+
],
382+
}
383+
`);
384+
385+
if (requests.length < 2) {
386+
return res(
387+
ctx.status(500),
388+
ctx.json({
389+
success: false,
390+
errors: [
391+
{
392+
code: 8000000,
393+
message: "Something exploded, please retry",
394+
},
395+
],
396+
messages: [],
397+
result: null,
398+
})
399+
);
400+
} else {
401+
return res.once(
402+
ctx.status(200),
403+
ctx.json({
404+
success: true,
405+
errors: [],
406+
messages: [],
407+
result: { url: "https://abcxyz.foo.pages.dev/" },
408+
})
409+
);
410+
}
411+
}
412+
),
413+
rest.post(
414+
"*/accounts/:accountId/pages/projects/foo/deployments",
415+
async (req, res, ctx) => {
416+
requests.push(req);
417+
expect(req.params.accountId).toEqual("some-account-id");
418+
expect(await (req as RestRequestWithFormData).formData())
419+
.toMatchInlineSnapshot(`
420+
FormData {
421+
Symbol(state): Array [
422+
Object {
423+
"name": "manifest",
424+
"value": "{\\"/logo.txt\\":\\"1a98fb08af91aca4a7df1764a2c4ddb0\\"}",
425+
},
426+
],
427+
}
428+
`);
429+
430+
return res.once(
431+
ctx.status(200),
432+
ctx.json({
433+
success: true,
434+
errors: [],
435+
messages: [],
436+
result: { url: "https://abcxyz.foo.pages.dev/" },
437+
})
438+
);
439+
}
440+
),
441+
rest.get(
442+
"*/accounts/:accountId/pages/projects/foo",
443+
async (req, res, ctx) => {
444+
expect(req.params.accountId).toEqual("some-account-id");
445+
446+
return res.once(
447+
ctx.status(200),
448+
ctx.json({
449+
success: true,
450+
errors: [],
451+
messages: [],
452+
result: { deployment_configs: { production: {}, preview: {} } },
453+
})
454+
);
455+
}
456+
)
457+
);
458+
459+
await runWrangler("pages deploy . --project-name=foo");
460+
461+
// Should be 2 attempts to POST /deployments
462+
expect(requests.length).toBe(2);
463+
301464
expect(normalizeProgressSteps(std.out)).toMatchInlineSnapshot(`
302465
"✨ Success! Uploaded 1 files (TIMINGS)
303466

packages/wrangler/src/api/pages/deploy.tsx

+31-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { validate } from "../../pages/validate";
2222
import { createUploadWorkerBundleContents } from "./create-worker-bundle-contents";
2323
import type { BundleResult } from "../../deployment-bundle/bundle";
2424
import type { Project, Deployment } from "@cloudflare/types";
25+
import { MAX_DEPLOYMENT_ATTEMPTS } from "../../pages/constants";
2526

2627
interface PagesDeployOptions {
2728
/**
@@ -363,12 +364,35 @@ export async function deploy({
363364
}
364365
}
365366

366-
const deploymentResponse = await fetchResult<Deployment>(
367-
`/accounts/${accountId}/pages/projects/${projectName}/deployments`,
368-
{
369-
method: "POST",
370-
body: formData,
367+
let attempts = 0;
368+
let lastErr: unknown;
369+
while (attempts < MAX_DEPLOYMENT_ATTEMPTS) {
370+
try {
371+
const deploymentResponse = await fetchResult<Deployment>(
372+
`/accounts/${accountId}/pages/projects/${projectName}/deployments`,
373+
{
374+
method: "POST",
375+
body: formData,
376+
}
377+
);
378+
return deploymentResponse;
379+
} catch (e) {
380+
lastErr = e;
381+
if (
382+
(e as { code: number }).code === 8000000 &&
383+
attempts < MAX_DEPLOYMENT_ATTEMPTS
384+
) {
385+
logger.debug("failed:", e, "retrying...");
386+
// Exponential backoff, 1 second first time, then 2 second, then 4 second etc.
387+
await new Promise((resolvePromise) =>
388+
setTimeout(resolvePromise, Math.pow(2, attempts++) * 1000)
389+
);
390+
} else {
391+
logger.debug("failed:", e);
392+
throw e;
393+
}
371394
}
372-
);
373-
return deploymentResponse;
395+
}
396+
// We should never make it here, but just in case
397+
throw lastErr;
374398
}

packages/wrangler/src/pages/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const MAX_BUCKET_SIZE = 50 * 1024 * 1024;
77
export const MAX_BUCKET_FILE_COUNT = 5000;
88
export const BULK_UPLOAD_CONCURRENCY = 3;
99
export const MAX_UPLOAD_ATTEMPTS = 5;
10+
export const MAX_DEPLOYMENT_ATTEMPTS = 3;
1011
export const MAX_CHECK_MISSING_ATTEMPTS = 5;
1112
export const SECONDS_TO_WAIT_FOR_PROXY = 5;
1213
export const isInPagesCI = !!process.env.CF_PAGES;

0 commit comments

Comments
 (0)