Repository navigation
fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors - #27537
Conversation
… the chunk door The chunk door read its index with parseInt and checked only isNaN: 0.5 read as 0, -1 reached the backend as part number 0 and answered 500, and an index at or past totalChunks was stored, counted in progress, and then blocked the completion door for good. The index is now read strictly (decimal digits only) and judged against the session's total_chunks after the resume-token check; anything else answers 400 INVALID_REQUEST naming the range, with nothing stored or counted. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
…te count The presigned door refused only a missing size, then judged Number(size) against the upload limit: a non-numeric size reached the sys_file insert and failed there as a 500, and a negative size stored a pending row. The size must now be a finite, non-negative JSON number; anything else answers 400 INVALID_REQUEST naming size, before any row is written. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
The upload-limit cases start a 1 MiB upload, which the init door floors to ONE declared chunk, then send chunk index 1. The chunk door now refuses an index past the declared count, so those cases widen the session row to two declared chunks; what they pin, the bytes received across chunks against the limit, is unchanged. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
…orage-route-input
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a200cfbda34f98cd236de0d10e5d937bd570f9c6 && git checkout a200cfbda34f98cd236de0d10e5d937bd570f9c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aef631b80b3175f2f43287a6e0185c828c356ac8 41272f9f5693500d15a712731557f41502a8d982 && git checkout -B drift-repro aef631b80b3175f2f43287a6e0185c828c356ac8 && git merge --no-ff 41272f9f5693500d15a712731557f41502a8d982
node scripts/docs-audit/affected-docs.mjs --json aef631b80b3175f2f43287a6e0185c828c356ac8
|
…② line GetPresignedUrlRequestSchema.size is a bare z.number(), so a negative size is inside the published accept set and the presigned door now refuses it: the declaration reads `no (narrowing)` and names it. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB 席位核对Verdict: REWORK (one item) at head
What is already checked and holds:
After the push, this PR owes a contract-tier review, recorded under Generated by Claude Code |
A `(narrowing)` arm declares a breaking change, and check-adr-0087-registration requires its disposition in writing: no migration prescription, since only a REST request value is refused. Claude-Session: https://claude.ai/code/session_017xRBcScm27Q3R5mkYLPWdB Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB Contract reviewServed-tier: Inputs read: cards #27412 and #27413 with every comment (triage, claims, both ① Derived judgments
② Semver level
③ Boundary flagsDev report 1 (comment 6495538727),
Dev report 2 (comment 6496406943),
④ Design judgment
Findings: must-fix none. Should-fix: the spec-lane follow-up card in ① (size Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB 席位核对Verdict: ACCEPT at head
Landing: ready to land, held. The landing step (relay Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
Fixes #27412
Fixes #27413
Clause-②: no (narrowing)
Metadata-protocol: none
Narrowed: a negative
size, which the spec number (GetPresignedUrlRequestSchema.size, a barez.number()) admits, is now refused400at the presigned door.维护者速读
What changes
Two upload doors in
@objectstack/service-storagenow refuse malformed request input at the door with400 INVALID_REQUEST, the code these doors already answer for a request they cannot take.Chunk door
PUT /api/v1/storage/upload/chunked/UPLOAD_ID/chunk/CHUNK_INDEX(#27412)parseIntread0.5as0, and1e0as1.total_chunks: it must be in[0, totalChunks). The check runs after the resume-token, organization and expiry checks (a caller without the token still gets403 INVALID_RESUME_TOKENand learns nothing of the range) and before a byte is read, stored or counted.chunkIndex must be 0: this upload declared 1 chunk, indexed from zero./chunkIndex must be a whole number from 0 to N: ...) and carriesdetails: { chunkIndex, totalChunks }.Presigned door
POST /api/v1/storage/upload/presigned(#27413)sizemust be a finite, non-negative JSON number (GetPresignedUrlRequestSchema.sizeisz.number()). It is judged after the required-field check and before the scope gate, the size limit and the pendingsys_fileinsert.size must be the file size in bytes.Exactly what is now refused (the narrowing the contract review reads)
chunkIndexpath segment that is not all decimal digits (-1,0.5,1e0,0x0,abc), and any whole number at or past the session'stotal_chunks. Before:-1answered500 INTERNAL;0.5was stored as chunk0; an out-of-range whole number answered200, was stored and counted, and then blockedcompletewith409for good.sizethat is a string (numeric strings such as"100"included),NaN, an infinity, a negative number, or a boolean. Before:"abc"answered500 INTERNAL(thesys_fileinsert's number validation);-5answered200and stored a pending row withsize: -5. A numeric string's previous answer depended on the engine's number-field handling at insert and was not measured here.0 .. totalChunks - 1; sizes0and any positive number. The SDK (storage.upload,resumeUpload) and the ConsoleUploadProvidersendFile.sizeand indexes in range.The completion door's own
unexpectedChunksrefusal stays: a session recorded before this change can still hold a stray chunk.Tests
chunked-upload-integrity.test.ts: new block for storage: the chunk door accepts chunk indexes outside the declared range and answers 500 on a negative index, leaving a session that can never complete #27412. Seven refused indexes (-1,1= totalChunks,7,0.5,1e0,0x0,abc), each asserting status400,error.codeINVALID_REQUEST, the range message, the details, that the backenduploadChunkwas never called, that the session's parts and progress are unchanged, and that a following in-range upload completes200with the right bytes. Controls: the multi-chunk range message, the last declared index accepted, and the403ordering before the index is judged. The existing stray-chunk completion case now plants its stray part on the session row (the shape a pre-fix session carries), since the chunk door no longer stores one.storage-routes.test.ts: new presign cases for storage: the presigned-upload door answers 500 INTERNAL for a non-numeric size and accepts a negative size #27413. Eight refused sizes, each asserting status400,error.codeINVALID_REQUEST, the message namingsize, and thatstore.createFilewas never called. Controls:0and1024answer200and store a pending row with that size.storage-limits.test.ts: two upload-limit cases started a 1 MiB upload (one declared chunk under the 5 MiB chunk floor) and then sent chunk index1. They now widen the session row to two declared chunks first; what they pin, bytes received across chunks against the limit, is unchanged.Acceptance notes
GetPresignedUrlRequestSchema.sizeinpackages/specisz.number()with no.int()or.min(0), so the spec still declares negative and fractional sizes legal while the door now refuses negatives. The door follows the dispatch's ruling (finite, non-negative); a fractional size is still accepted. Not filed: no producer sends either.storage-limits.test.tsfixture update is a consequence of the storage: the chunk door accepts chunk indexes outside the declared range and answers 500 on a negative index, leaving a session that can never complete #27412 refusal and sits in its own commit after the two card commits, because folding it into the first commit needed a history rewrite that was not permitted in this session.Verification
Package tests and typecheck at dbfaf81 (before the merge of origin/main; the merged commit touches no service-storage file)
The first run, before the
storage-limits.test.tsfixture update, failed the two limit cases named above (Tests 2 failed | 1276 passed).Ablation, one per guard (source mutation through scripts/ablation-replace.mjs; the suites import the routes from src, so no dist build is involved)
With the range check removed,
-1,0.5,1e0,0x0andabcstay refused by the strict parse, as expected.Derived gate families (dispatch-gates --commands at 3a10b29)
Those three are NOT MEASURED locally and are left to CI.
Generated by Claude Code