peekBody() reads up to a kilobyte of a body whose length nobody stated, so the compression threshold can be applied without buffering a real stream. It then always hands back a ReadableStream that re-emits the prefix and pulls the rest.
When the body ended inside the prefix there is no rest. It was never a stream - it was a small answer that arrived whole - and rebuilding it as one asks Bun for a controller, a start microtask and a drain on every response.
Two things follow from that:
- The synthesized stream is pure overhead for the common case, and its
start/pullmachinery dominated the profile of any response without aContent-Length. - The peek counted the bytes and then discarded the count, so the next thing to look at that body has to open it again to learn what this code already knew.
Fix
peekBody returns bytes when the body ended inside the prefix and a stream only when there is genuinely more to come, and compressUnknownLengthResponse states the Content-Length it just counted. peekedStream() turns a peeked body back into a stream for the branch that has to pipe it.
The reading itself is unchanged: a body still in flight is still peeked and still streams, prefix re-attached, one chunk at a time.
Note for anyone measuring this
Reading an in-memory body through a stream reader is far more expensive than it looks. Measured at a fixed 8,000 req/s, microseconds of server CPU per request for a 17-byte JSON response:
| us/req | |
|---|---|
| return the Response | 20.8 |
touch .body | 22.0 |
await response.text() and rebuild | 22.7 |
getReader() + read to completion + rebuild | 36.7 |
A reader loop costs roughly eight times what a buffered read of the same bytes costs. This change removes one of the two streams involved; the source stream is still opened when a length is unknown, which is why stating Content-Length wherever it is already known matters more than any of this.