diff --git a/.changeset/improve-asset-upload-retry-log.md b/.changeset/improve-asset-upload-retry-log.md new file mode 100644 index 0000000000..3347e64632 --- /dev/null +++ b/.changeset/improve-asset-upload-retry-log.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Improve the log message shown when an asset upload attempt fails and is retried + +The retry message now reports which attempt is being made (e.g. `Asset upload failed. Retrying... 1 of 5 attempts.`), making it easier to gauge how close Wrangler is to exhausting its retry budget. The raw error object is no longer appended to this user-facing message; it is instead logged at debug level (visible via `WRANGLER_LOG=debug`). diff --git a/packages/wrangler/src/__tests__/deploy/assets.test.ts b/packages/wrangler/src/__tests__/deploy/assets.test.ts index 44e18c714b..0c9644b400 100644 --- a/packages/wrangler/src/__tests__/deploy/assets.test.ts +++ b/packages/wrangler/src/__tests__/deploy/assets.test.ts @@ -1326,5 +1326,76 @@ describe("deploy", () => { `[Error: An unexpected response has been received from the Cloudflare API for assets upload. Please try again.]` ); }); + + it("should retry asset uploads on failure and log a retry message including the attempt count", async ({ + expect, + }) => { + vi.stubEnv("WRANGLER_LOG", "debug"); + + const assets = [{ filePath: "file-1.txt", content: "Content of file-1" }]; + writeAssets(assets); + writeWranglerConfig({ + assets: { directory: "assets" }, + }); + + const mockBuckets = [["0de3dd5df907418e9730fd2bd747bd5e"]]; + await mockAUSRequest([], mockBuckets, "<>"); + + // Fail the first upload attempt, succeed on the second. + const uploadAttempts: Request[] = []; + msw.use( + http.post( + "*/accounts/some-account-id/workers/assets/upload", + async ({ request }) => { + uploadAttempts.push(request); + if (uploadAttempts.length === 1) { + return HttpResponse.json( + { + success: false, + errors: [{ code: 1, message: "upload-boom-from-test" }], + messages: [], + result: null, + }, + { status: 500 } + ); + } + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { jwt: "<>" }, + }, + { status: 201 } + ); + } + ) + ); + + mockSubDomainRequest(); + mockUploadWorkerRequest({ + expectedAssets: { + jwt: "<>", + config: {}, + }, + expectedType: "none", + }); + + await runWrangler("deploy"); + + // The upload endpoint was hit twice: once failing, once succeeding. + expect(uploadAttempts.length).toBe(2); + + // The new info message includes the attempt count. + expect(std.info).toContain( + "Asset upload failed. Retrying... 1 of 5 attempts." + ); + + // The error details no longer leak into the user-facing info log. + expect(std.info).not.toContain("upload-boom-from-test"); + + // The error is now logged at debug level instead. + expect(std.debug).toContain("upload-boom-from-test"); + }); }); }); diff --git a/packages/wrangler/src/assets.ts b/packages/wrangler/src/assets.ts index 7637ef9f0e..42b1cd88c0 100644 --- a/packages/wrangler/src/assets.ts +++ b/packages/wrangler/src/assets.ts @@ -193,7 +193,12 @@ export const syncAssets = async ( return res; } catch (e) { if (attempts < MAX_UPLOAD_ATTEMPTS) { - logger.info(chalk.dim(`Asset upload failed. Retrying...\n`, e)); + logger.info( + chalk.dim( + `Asset upload failed. Retrying... ${attempts + 1} of ${MAX_UPLOAD_ATTEMPTS} attempts.\n` + ) + ); + logger.debug(e); // Exponential backoff, 1 second first time, then 2 second, then 4 second etc. await new Promise((resolvePromise) => setTimeout(resolvePromise, Math.pow(2, attempts) * 1000)