Skip to content

Commit e5a160a

Browse files
Brooooooklynclaude
andcommitted
fix(platform): check for a saved upload when a replay's 4xx body drops
After an unclear failure, the replay is rejected with a 4xx. If that response body dropped, the deploy failed before the saved-upload check ran. It now runs the same check first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 204b6da commit e5a160a

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎platform/packages/api/src/deploy/upload-worker.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,6 @@ export async function uploadDeploymentWorker(input: {
516516
if (failure instanceof WorkerModuleIntegrityError) {
517517
throw failure;
518518
}
519-
// A 4xx stays final even when its body drops. Any other failure is
520-
// retried.
521-
if (cfResponse && !cfResponse.ok && cfResponse.status < 500) {
522-
throw new Error(`worker upload failed (HTTP ${cfResponse.status})`, { cause: error });
523-
}
524519
const timedOut = (error as { name?: unknown } | null)?.name === 'TimeoutError';
525520
const cause = failure ?? error;
526521
const fields = {
@@ -531,6 +526,14 @@ export async function uploadDeploymentWorker(input: {
531526
status: cfResponse?.status,
532527
err: cause instanceof Error ? cause.message : String(cause),
533528
};
529+
// A 4xx stays final even when its body drops, unless it replays an
530+
// upload that was saved. Any other failure is retried.
531+
if (cfResponse && !cfResponse.ok && cfResponse.status < 500) {
532+
if (maybeSaved && (await savedDespiteFailure(fields))) {
533+
return true;
534+
}
535+
throw new Error(`worker upload failed (HTTP ${cfResponse.status})`, { cause: error });
536+
}
534537
if (lastAttempt) {
535538
// This attempt or an earlier one may have been saved.
536539
if (await savedDespiteFailure(fields)) {

‎platform/packages/api/test/integration/api-deploy.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2866,6 +2866,41 @@ describe('POST /projects/:id/deploy', () => {
28662866
expect(dispatchScriptReads(cf)).toBe(1);
28672867
});
28682868

2869+
it('treats an upload saved before a replay whose 4xx body drops as done', async () => {
2870+
const user = await createTestUser(env.db);
2871+
const project = await createTestProject(env.db, user.id, 'worker-upload-saved-4xx-body');
2872+
const savedLogs = captureSavedUploadLogs();
2873+
let uploadCalls = 0;
2874+
const cf: MockCfApiHandle = mockCfApi({
2875+
uploadWorker: async (url, init) => {
2876+
uploadCalls++;
2877+
if (uploadCalls === 1) {
2878+
await saveScript(cf, url, init);
2879+
throw new TypeError('fetch failed');
2880+
}
2881+
return brokenBodyResponse(400);
2882+
},
2883+
});
2884+
2885+
const res = await app.fetch(await buildDeployRequest(env, project.id, user.jwt), env);
2886+
2887+
expect(res.status).toBe(200);
2888+
const events = parseDeployStream(await res.text());
2889+
expect(events.at(-1)).toMatchObject({ event: 'done' });
2890+
expect(uploadCalls).toBe(2);
2891+
expect(dispatchScriptReads(cf)).toBe(1);
2892+
expect(savedLogs()).toEqual([
2893+
expect.objectContaining({
2894+
attempt: 1,
2895+
maxAttempts: 4,
2896+
latencyMs: expect.any(Number),
2897+
code: 'transport',
2898+
status: 400,
2899+
err: 'terminated',
2900+
}),
2901+
]);
2902+
});
2903+
28692904
it('treats an upload saved before the last attempt times out as done', async () => {
28702905
const user = await createTestUser(env.db);
28712906
const project = await createTestProject(env.db, user.id, 'worker-upload-saved-timeout');

0 commit comments

Comments
 (0)