Conversation
Some runtimes resolve undici to builds whose request() body has no dump(). discardBody then threw TypeError and its fallback body.destroy(error) re-emitted the error on a stream without an error listener, crashing the host process with uncaughtException. Use dump when available, otherwise drain the stream with a size cap. Discard failures are swallowed and the stream is destroyed quietly.
|
Thank you, @Indosaram — this caught the remaining lifecycle edge beyond the initial Bun compatibility fix. I adapted the bounded drain + error-guard approach into Senpi in code-yeongyu/senpi#1094, with explicit credit in both the PR and changelog. The Senpi adaptation also extracts response-body cleanup into its own focused module to keep the vendored fetcher below our 250 pure-LOC ceiling. Local RED→GREEN, focused tests, root checks, and the real Bun redirect reproduction are green; Senpi CI/review is running now. 🙌 |
Adapt the lifecycle hardening proposed by @Indosaram in code-yeongyu/pi-webfetch#7 while preserving Senpi's bounded Undici dump path. Bodies without dump() are drained before quiet destruction, and discard-time stream errors are guarded from escaping the process.
code-yeongyu
left a comment
There was a problem hiding this comment.
Reviewed against current main (which already contains #8 / 6e963d0).
The dump() feature-detect is already on main. The remaining unique value is the no-dump drain fallback and the unit tests. I will land that remainder (with two mechanical adjustments below) and close this PR with credit.
Findings:
src/webfetch/fetcher.ts:226(finallyindiscardBody): thebody.on?.("error", …)listener is attached afterdump()/drainBody(). A stream that errors during discard can emit before the listener exists. Attach the no-op listener before dump/drain, thendestroy()infinally.src/webfetch/fetcher.ts:232(drainBody): thefor awaitloop does not honorAbortSignal. Discard during timeout/abort can keep the iterator alive untilMAX_RESPONSE_SIZE_BYTES. Race the iterator against abort, matching the fetch read path.src/webfetch/fetcher.ts:48(ResponseBodyStream): makingdumpoptional is correct (already on main). Keepon?/once?optional so Bun-compatible bodies without an EventEmitter API still type-check.
test/discard-body.test.ts is the right regression surface; I will keep those cases (no-dump drain, mid-stream error swallowed, dump path used when present).
|
Superseded by #10 (merged in 169155b), which lands the remaining drain fallback from this PR: feature-detect dump(), drain when dump is missing, attach a quiet error listener before teardown, honor AbortSignal, and add discard-body regression tests. Credit to @Indosaram (Co-authored-by). |
Problem
Running the extension inside a host whose module resolution maps
undicito a build whererequest()bodies lackdump()crashed the entire host process with:Two defects combined here:
discardBodyassumedbody.dump()always exists. In some runtimes (bundled loaders, alternate undici resolutions) the response body is a plain stream without it, so discarding a redirect or oversized body throws.body.destroy(error), re-emitting the caught error on a stream that has noerrorlistener. An unhandled streamerrorevent escapes as an uncaught exception and kills the host agent process instead of failing a single tool call.Fix
dumpoptional onResponseBodyStreamand feature-check before calling it.dumpis unavailable, drain the body via async iteration bounded byMAX_RESPONSE_SIZE_BYTES.errorlistener beforedestroy()so nothing can escape as unhandled.Testing
test/discard-body.test.ts: body without dump drains and resolves; mid-stream failure resolves without surfacing; dump-capable body still usesdump.npm run checkand fullnpm testpass (43 tests).Summary by cubic
Prevents host crashes by making discardBody safe when response bodies lack dump in environments resolving
undicito builds without it. Previously it calleddump()and thendestroy(error), causing a TypeError or an unhandled stream error; now it feature-checksdump, drains with a size cap, and destroys quietly.body.dump({ limit: 1024 })when available; otherwise drains via async iteration bounded by MAX_RESPONSE_SIZE_BYTES.errorlistener beforedestroy()to avoid uncaught exceptions.dumpexists; only failure handling differs.dump, mid-stream failures, anddump-capable bodies.Written for commit 8a4743d. Summary will update on new commits.