Errored response-body streams terminate cleanly instead of aborting, silently truncating the response
Summary
When a Response is constructed from a ReadableStream and that stream errors after the response headers have been sent, workerd finishes the HTTP response with a normal chunked-encoding terminator (0\r\n\r\n) and leaves the connection intact. The downstream HTTP client therefore sees a protocol-complete, successful response containing a truncated body — it has no way to tell that the worker actually failed.
The uncaught exception is logged to stderr in the default logging mode, but it is not surfaced through the HTTP response, the status code, or (critically) structured logging. There is no programmatic signal a consumer can rely on.
This makes any tool that consumes a worker over HTTP — most notably build-time prerendering — unable to detect a failed render. The downstream symptom is a build that writes truncated HTML and exits 0.
Downstream report: withastro/astro#17047 (Astro static prerendering with the Cloudflare adapter)
Expected behavior
When a streaming response body errors after headers are sent, workerd should abort the HTTP response (e.g. reset the connection / signal an incomplete message) rather than emit a clean final chunk. The consumer should observe an incomplete-read / aborted-stream error, matching how Node.js and other runtimes surface a stream that errors mid-flight.
Ideally the uncaught exception should also be emitted via structured logging (--structured-logging) so it remains observable when JSON logging is enabled.
Actual behavior
- HTTP status is
200.
- The chunked body is terminated normally (
0\r\n\r\n) — the connection is not reset.
- The consumer reads a truncated (or empty) body with no error.
- The exception is written to stderr only in the default (non-structured) logging mode. With
--structured-logging enabled it is not surfaced anywhere.
Minimal reproduction (standalone workerd)
worker.js:
export default {
async fetch(request) {
let n = 0;
const stream = new ReadableStream({
pull(controller) {
if (n === 0) {
controller.enqueue(new TextEncoder().encode("<html><head>chunk0\n"));
n++;
return;
}
// Error the body stream after headers have already been sent
controller.error(new Error("BOOM mid-stream"));
},
});
return new Response(stream, { headers: { "content-type": "text/html" } });
},
};
config.capnp:
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [ (name = "main", worker = .mainWorker) ],
sockets = [ (name = "http", address = "*:8799", http = (), service = "main") ],
);
const mainWorker :Workerd.Worker = (
modules = [ (name = "worker.js", esModule = embed "worker.js") ],
compatibilityDate = "2025-06-01",
);
Run and request:
$ workerd serve config.capnp
$ curl -sv --raw http://localhost:8799/
Observed response:
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
< Content-Type: text/html
<
13
<html><head>chunk0
0
* Connection #0 to host localhost left intact
curl exits 0. The 0\r\n\r\n final chunk indicates a complete message; the client cannot detect the failure. stderr shows:
workerd/server/server.c++: error: Uncaught exception: ... remote.jsg.Error: BOOM mid-stream
A throw inside pull() (instead of controller.error(...)) behaves identically.
Related case: background unhandled rejection
A fire-and-forget rejection after the response is returned is also fully silent (status 200, full body, no error, no structured log):
export default {
async fetch() {
Promise.reject(new Error("BOOM unhandled rejection"));
return new Response("<html><head>ok", { headers: { "content-type": "text/html" } });
},
};
Additional finding: structured logging suppresses the stderr signal
The exception text above only appears on stderr in the default logging mode. When the runtime is launched with structured logging enabled (structuredLogging = true in the runtime config, which Miniflare/Wrangler enable when a structured-log handler is registered), the uncaught exception is emitted nowhere — not as a structured log entry and not on stderr. This removes the last remaining (already environment-dependent) signal in exactly the configuration that tools like the Cloudflare Vite plugin run under during builds.
Impact
Any consumer of a worker over HTTP cannot reliably detect a failed streaming response:
- Build-time prerendering (Astro Cloudflare adapter
prerenderEnvironment: 'workerd') writes truncated HTML to dist/ and exits 0, so CI reports success on a broken build.
- Error visibility is OS-dependent (the downstream report notes Linux shows the stderr line, macOS does not) because the only signal is unstructured stderr text.
Proposed fix
- When a response body
ReadableStream errors after the head has been sent, abort the HTTP response (reset the connection / terminate the chunked stream abnormally) instead of writing a clean 0\r\n\r\n terminator, so the consumer observes an incomplete/aborted read.
- Emit the uncaught exception through structured logging so it remains observable when
--structured-logging is enabled.
Environment
- Reproduced standalone with
workerd serve (workerd@1.20260423.1) and confirmed at the raw HTTP layer with curl; also reproduced via Miniflare (miniflare@4.x, workers-sdk main).
- compatibilityDate:
2025-06-01
- Platforms observed: macOS (stderr suppressed). Downstream report also covers Linux (stderr shown).
Errored response-body streams terminate cleanly instead of aborting, silently truncating the response
Summary
When a
Responseis constructed from aReadableStreamand that stream errors after the response headers have been sent, workerd finishes the HTTP response with a normal chunked-encoding terminator (0\r\n\r\n) and leaves the connection intact. The downstream HTTP client therefore sees a protocol-complete, successful response containing a truncated body — it has no way to tell that the worker actually failed.The uncaught exception is logged to stderr in the default logging mode, but it is not surfaced through the HTTP response, the status code, or (critically) structured logging. There is no programmatic signal a consumer can rely on.
This makes any tool that consumes a worker over HTTP — most notably build-time prerendering — unable to detect a failed render. The downstream symptom is a build that writes truncated HTML and exits
0.Downstream report: withastro/astro#17047 (Astro static prerendering with the Cloudflare adapter)
Expected behavior
When a streaming response body errors after headers are sent, workerd should abort the HTTP response (e.g. reset the connection / signal an incomplete message) rather than emit a clean final chunk. The consumer should observe an incomplete-read / aborted-stream error, matching how Node.js and other runtimes surface a stream that errors mid-flight.
Ideally the uncaught exception should also be emitted via structured logging (
--structured-logging) so it remains observable when JSON logging is enabled.Actual behavior
200.0\r\n\r\n) — the connection is not reset.--structured-loggingenabled it is not surfaced anywhere.Minimal reproduction (standalone workerd)
worker.js:config.capnp:Run and request:
Observed response:
curlexits0. The0\r\n\r\nfinal chunk indicates a complete message; the client cannot detect the failure. stderr shows:A
throwinsidepull()(instead ofcontroller.error(...)) behaves identically.Related case: background unhandled rejection
A fire-and-forget rejection after the response is returned is also fully silent (status
200, full body, no error, no structured log):Additional finding: structured logging suppresses the stderr signal
The exception text above only appears on stderr in the default logging mode. When the runtime is launched with structured logging enabled (
structuredLogging = truein the runtime config, which Miniflare/Wrangler enable when a structured-log handler is registered), the uncaught exception is emitted nowhere — not as a structured log entry and not on stderr. This removes the last remaining (already environment-dependent) signal in exactly the configuration that tools like the Cloudflare Vite plugin run under during builds.Impact
Any consumer of a worker over HTTP cannot reliably detect a failed streaming response:
prerenderEnvironment: 'workerd') writes truncated HTML todist/and exits0, so CI reports success on a broken build.Proposed fix
ReadableStreamerrors after the head has been sent, abort the HTTP response (reset the connection / terminate the chunked stream abnormally) instead of writing a clean0\r\n\r\nterminator, so the consumer observes an incomplete/aborted read.--structured-loggingis enabled.Environment
workerd serve(workerd@1.20260423.1) and confirmed at the raw HTTP layer withcurl; also reproduced via Miniflare (miniflare@4.x, workers-sdkmain).2025-06-01