fix(client): resumeUpload exits on an expired session instead of uploading into it - #8267
Conversation
…ading into it (#7870) `storage.resumeUpload` polls progress before sending anything, but destructured only `totalChunks` / `uploadedChunks` and discarded `status`. Since #7667 a session past its own `expires_at` is durably stamped `expired` and reported by that poll, so resume walked into the chunk loop and learned the session was dead from the 410 `UPLOAD_SESSION_EXPIRED` its first chunk PUT returned -- an honest failure that spent a whole chunk upload to rediscover what it already held. `expired` now short-circuits before the file is read, throwing an Error carrying `code: 'UPLOAD_SESSION_EXPIRED'` and `httpStatus: 410` -- the registered code and status the server answers this same condition with, so a caller's existing branch keeps matching. Compared with `=== 'expired'` exactly: every other declared status resumes as before, and an absent `status` cannot misfire it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pY4Xb2iDecfDtT3CWoiTW
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #7870
What
storage.resumeUploadpollsGET /api/v1/storage/upload/chunked/:uploadId/progressbefore it sends anything, but destructured onlytotalChunks/uploadedChunksoff the response and discardedstatus.Since #7667 (PR #7844) a session past its own
expires_atis durably stampedexpired, and that poll reports it. So a client resuming a dead session learned nothing from the response already in hand, read the file, uploaded a full chunk, and discovered the expiry from the 410UPLOAD_SESSION_EXPIREDits first chunk PUT came back with. Honest, but it spent an upload to rediscover something it had been told.The poll's
statusis now read.expiredshort-circuits before the file is read or a single byte leaves.The error shape is the server's, not a new one
The thrown error carries
code: 'UPLOAD_SESSION_EXPIRED'andhttpStatus: 410— deliberately the same registered code and status the server answers a chunk PUT against that session with, plusdetails: { uploadId, expiresAt }.error.codewas minted.UPLOAD_SESSION_EXPIREDis already registered inERROR_CODE_LEDGER(packages/spec/src/api/error-code-ledger.zod.ts), so the ADR-0112 closed vocabulary is untouched.fetchwrapper uses for any non-2xx —new Error(message)withcode/httpStatus/detailsattached. This matches it exactly, so a caller already branching onerr.code === 'UPLOAD_SESSION_EXPIRED'keeps matching. The only difference is how early it fires, and that the bytes stay home.UploadProgressSchema.statusalready declaresexpired; this only consumes a value that was already on the wire.packages/specis untouched.Scope
status === 'expired'only, per the grading comment. The guard compares against'expired'exactly rather than testing truthiness, so every other declared status (in_progress,completing,completed,failed) resumes exactly as before —failedand wider status handling stay out — and a server or fixture that omitsstatuscannot misfire it. That last case is live, not hypothetical: the SDK's own URL-conformance fixture drives this method with a counters-only body.Landed in the
storage.resumeUploadarm only. The widerPromise-of-anysweep over this same file is #8140's, and is not touched here.Tests
Four cases in
packages/client/src/storage-wire-dialect.test.ts, asserting the envelope (codeandhttpStatus) rather than merely that something threw — a bareErrorwould satisfy "it throws" while breaking every caller the fix exists to serve.Reverse verification, direction predicted before running: with the guard removed, the two expiry cases go red and the two non-misfire cases stay green.
That second failure is the defect stated numerically: pre-fix, resume made three requests against a session already reported dead. Restored byte-identically afterwards (
git hash-object7ea8a55c9d0217ea8521ce27af7bfd814fb12294 before and after).Green with the fix in place:
pnpm --filter @objectstack/client test— 23 files / 300 tests passed.pnpm --filter @objectstack/client typecheck— clean.Generated by Claude Code