Repository navigation
Conversation
GCS getAsStream returned createReadStream() without any I/O, so a missing object resolved successfully and failed later as a stream 'error'. A caller doing reply.send(await getAsStream(k)) sent a 200 with a broken body, or crashed the process on an unhandled error. Every other provider rejects. GCS is the production backend. It now checks existence first. Plugin dispatch read Configure[opts.type] straight off an object literal, so type:"toString" resolved Object.prototype.toString and registered successfully with no FileStore decorated — a typo became a silent no-op boot rather than a failure. streamToBuffer only handled data/error/end. A stream destroyed without an error emits close alone, leaving the promise pending forever and hanging the request that awaited getAsBuffer. It now rejects, and a settled guard keeps late events from settling twice. Local getAsBuffer/getAsStream wrapped reads in if (await stat(p)), which can never be falsy since stat throws — so the File not found branch was unreachable and callers got raw ENOENT. Two specs reached it only by mocking stat to resolve undefined, a state the real filesystem never produces, and asserted an error the code cannot throw. getAsBuffer now reads directly; getAsStream keeps the stat so a missing file rejects rather than deferring to a stream error, matching the GCS fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 14, 2026
Member
Author
|
Superseded by #23, which carries these commits unchanged. Splitting this into separate reviews added overhead without adding safety. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch 1 of the review findings — the correctness fixes. Each has a regression test that fails if reverted. 136 tests, 100% coverage held, MinIO integration green.
GCS
getAsStreamresolved for missing objectsreturn gcsfile.createReadStream()did no I/O before returning, so a missing object resolved successfully and the 404 arrived later as a stream'error'. Every other provider rejects: S3 awaitsGetObject, Azure'sdownload()throws, local throws ENOENT.A caller doing
reply.send(await FileStore.getAsStream(k))therefore sent a 200 with a broken body on GCS, or took the process down on an unhandled'error'. GCS is the production backend, so this was the one finding with live exposure.Now checks existence first. Costs an extra round trip on that path; correctness is worth it, and the test asserts
createReadStreamis not even called for a missing object.Plugin dispatch accepted
Object.prototypenamesConfigure[opts.type]walked the prototype chain. Demonstrated before the fix:A typo booted successfully with nothing decorated, turning into a TypeError on first use rather than a boot failure. Now guarded with
hasOwnProperty.call; all three reject withUnknown storage type.Object.hasOwnwould read better butlibis ES2020 — widening it is a separate change, not a side effect of this one.streamToBuffercould hang foreverOnly
data/error/endwere handled. A stream destroyed without an error argument emits'close'alone, leaving the promise pending and hanging whichever request awaitedgetAsBuffer— on Azure, GCS and S3.Now rejects on premature close, with a
settledguard so late events cannot settle twice. The normal path (endthenclose) is unaffected.The reviewing agent was honest that it could not trigger this through a real provider — MinIO gives ECONNRESET, Azure destroys with an error — so the trigger needs a stream closed cleanly without one. Unproven in situ, cheap to defend.
Dead branch on local reads, and the tests that enshrined it
getAsBufferandgetAsStreamwrapped reads inif (await fs.promises.stat(p)). A fulfilledstatis never falsy, sothrow new Error("File not found: ...")was unreachable and callers actually got raw ENOENT.Two specs reached it only by mocking
statto resolveundefined— a state the real filesystem never produces — and asserted an error the code cannot throw. Anyone reading them would believe missing files throwFile not found. They were covering dead code to satisfy the 100% threshold.getAsBuffernow reads directly (also dropping a redundant stat and its TOCTOU window).getAsStreamkeeps the stat deliberately:createReadStreamis lazy like the GCS reader, so without it a missing file would defer to a stream'error'— the same bug fixed above. Both specs now assert the real ENOENT.Verification
Behaviour confirmed against the built
dist/: the three prototype names now throw, and localgetAsStreamon a missing file rejects with ENOENT.Not in this batch
Deliberately held for a separate change, since each needs a decision rather than a fix:
HeadBucketprobe would catch it, but a pod with object-level IAM and no bucket-level permission would then fail to boot — that trade-off is yours, and a non-fatal warning may be the right shape.save/uploadrejections survive the suite today.