denoland/deno · error
stream reader has shut down
Error message
stream reader has shut down
What it means
ExternallyAbortableReaderStream (ext/http/reader_stream.rs) adapts a tokio AsyncRead into the hyper response body — used for auto-compressed bodies, where the handler writes into a duplex pipe and this stream feeds hyper. A clean end requires ShutdownHandle::shutdown(), set by op_http_shutdown when the response finishes; if the reader hits EOF without that flag, the stream emits io::ErrorKind::UnexpectedEof 'stream reader has shut down' so the truncated body aborts the transfer instead of appearing successful.
Source
Thrown at ext/http/reader_stream.rs:58
inner: ReaderStream::new(reader),
done: done.clone(),
};
(this, ShutdownHandle(done))
}
}
impl<R: AsyncRead> Stream for ExternallyAbortableReaderStream<R> {
type Item = std::io::Result<Bytes>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
let this = self.project();
let val = std::task::ready!(this.inner.poll_next(cx));
match val {
None if this.done.load(Ordering::SeqCst) => Poll::Ready(None),
None => Poll::Ready(Some(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"stream reader has shut down",
)))),
Some(val) => Poll::Ready(Some(val)),
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use deno_core::futures::StreamExt;
use tokio::io::AsyncWriteExt;
use super::*;
#[tokio::test]
async fn success() {View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Wrap the streaming body generator in try/finally and finish or explicitly close/abort the writer on error
- Do not write any body bytes before you are certain the response can complete
- Handle the upstream error before any bytes reach the compressed writer
- On the client side, treat UnexpectedEof on a content-length'd response as partial data and retry idempotent requests
Example fix
// before
new Response(
(async function* () {
yield part1;
throw new Error('boom'); // body abandoned mid-stream
})(),
);
// after
new Response(
(async function* () {
try {
yield part1;
throw new Error('boom');
} catch (e) {
console.error('body failed', e);
yield encoder.encode('partial content'); // or close cleanly
}
})(),
); Defensive patterns
Strategy: try-catch
Try / catch
// server side: never abandon a partially written body
const body = new ReadableStream({
async start(controller) {
try {
for await (const chunk of source()) controller.enqueue(chunk);
controller.close();
} catch (e) {
controller.error(e); // client sees a clean stream error, not a truncated body
}
},
});
// client side: treat EOF-without-end as partial content
const res = await fetch(url);
const text = await res.text().catch((e) => {
if (/unexpected EOF|stream reader has shut down/i.test(String(e))) {
throw new Error('Partial response body; safe to retry');
}
throw e;
}); Prevention
- Finish or explicitly close/abort every response body writer before the handler returns
- Validate inputs before writing the first body byte so errors happen header-only
- For generators that may fail mid-stream, error the controller instead of returning
- Retry idempotent requests when a content-length'd body ends with UnexpectedEof
When it happens
Trigger: A Deno.serve handler writes part of a compressible (gzip/brotli auto-negotiated) body and then abandons it: the handler throws mid-write, the response body writer is dropped without a proper close/abort, or the shutdown path is never invoked because the handler errored while the compressed body was partially streamed.
Common situations: Streaming generators that throw after emitting some chunks; handlers returning early after partial writes; clients observing dropped connections with truncated content-length'd bodies behind proxies.
Related errors
- ERR_INVALID_ARG_TYPE
- ERR_INVALID_URL
- ERR_UNESCAPED_CHARACTERS
- ERR_INVALID_PROTOCOL
- ERR_HTTP_HEADERS_SENT
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/8b6843b3b449546e.
Report an issue: GitHub.