mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 12:29:07 +00:00
## Summary Fixes #18413 - Empty chunked gzip responses were causing `Decompression error: ShortRead` ## The Issue When a server sends an empty response with `Content-Encoding: gzip` and `Transfer-Encoding: chunked`, Bun was throwing a `ShortRead` error. This occurred because the code was checking if `avail_in == 0` (no input data) and immediately returning an error, without attempting to decompress what could be a valid empty gzip stream. ## The Fix Instead of checking `avail_in == 0` before calling `inflate()`, we now: 1. Always call `inflate()` even when `avail_in == 0` 2. Check the return code from `inflate()` 3. If it returns `BufError` with `avail_in == 0`, then we truly need more data and return `ShortRead` 4. If it returns `StreamEnd`, it was a valid empty gzip stream and we finish successfully This approach correctly distinguishes between "no data yet" and "valid empty gzip stream". ## Why This Works - A valid empty gzip stream still has headers and trailers (~20 bytes) - The zlib `inflate()` function can handle empty streams correctly - `BufError` with `avail_in == 0` specifically means "need more input data" ## Test Plan ✅ Added regression test in `test/regression/issue/18413.test.ts` covering: - Empty chunked gzip response - Empty non-chunked gzip response - Empty chunked response without gzip ✅ Verified all existing gzip-related tests still pass ✅ Tested with the original failing case from the issue 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
184 lines
4.8 KiB
TypeScript
184 lines
4.8 KiB
TypeScript
import { serve } from "bun";
|
|
import { expect, test } from "bun:test";
|
|
|
|
/**
|
|
* Comprehensive test to ensure all compression algorithms handle empty streams correctly
|
|
* Related to issue #18413 - we fixed this for gzip, now verifying brotli and zstd work too
|
|
*/
|
|
|
|
test("empty chunked brotli response should work", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty brotli buffer using the proper API
|
|
const { brotliCompressSync } = require("node:zlib");
|
|
const emptyBrotli = brotliCompressSync(Buffer.alloc(0));
|
|
|
|
// Return as chunked response
|
|
return new Response(
|
|
new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(emptyBrotli);
|
|
controller.close();
|
|
},
|
|
}),
|
|
{
|
|
headers: {
|
|
"Content-Encoding": "br",
|
|
"Transfer-Encoding": "chunked",
|
|
"Content-Type": "text/plain",
|
|
},
|
|
},
|
|
);
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Should not throw decompression error
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|
|
|
|
test("empty non-chunked brotli response", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty brotli buffer using the proper API
|
|
const { brotliCompressSync } = require("node:zlib");
|
|
const emptyBrotli = brotliCompressSync(Buffer.alloc(0));
|
|
|
|
return new Response(emptyBrotli, {
|
|
headers: {
|
|
"Content-Encoding": "br",
|
|
"Content-Type": "text/plain",
|
|
"Content-Length": emptyBrotli.length.toString(),
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|
|
|
|
test("empty chunked zstd response should work", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty zstd buffer using the proper API
|
|
const emptyZstd = Bun.zstdCompressSync(Buffer.alloc(0));
|
|
|
|
// Return as chunked response
|
|
return new Response(
|
|
new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(emptyZstd);
|
|
controller.close();
|
|
},
|
|
}),
|
|
{
|
|
headers: {
|
|
"Content-Encoding": "zstd",
|
|
"Transfer-Encoding": "chunked",
|
|
"Content-Type": "text/plain",
|
|
},
|
|
},
|
|
);
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Should not throw decompression error
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|
|
|
|
test("empty non-chunked zstd response", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty zstd buffer using the proper API
|
|
const emptyZstd = Bun.zstdCompressSync(Buffer.alloc(0));
|
|
|
|
return new Response(emptyZstd, {
|
|
headers: {
|
|
"Content-Encoding": "zstd",
|
|
"Content-Type": "text/plain",
|
|
"Content-Length": emptyZstd.length.toString(),
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|
|
|
|
test("empty chunked deflate response should work", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty deflate buffer
|
|
const emptyDeflate = Bun.deflateSync(Buffer.alloc(0));
|
|
|
|
// Return as chunked response
|
|
return new Response(
|
|
new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(emptyDeflate);
|
|
controller.close();
|
|
},
|
|
}),
|
|
{
|
|
headers: {
|
|
"Content-Encoding": "deflate",
|
|
"Transfer-Encoding": "chunked",
|
|
"Content-Type": "text/plain",
|
|
},
|
|
},
|
|
);
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Should not throw decompression error
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|
|
|
|
test("empty non-chunked deflate response", async () => {
|
|
using server = serve({
|
|
port: 0,
|
|
async fetch(req) {
|
|
// Create an empty deflate buffer
|
|
const emptyDeflate = Bun.deflateSync(Buffer.alloc(0));
|
|
|
|
return new Response(emptyDeflate, {
|
|
headers: {
|
|
"Content-Encoding": "deflate",
|
|
"Content-Type": "text/plain",
|
|
"Content-Length": emptyDeflate.length.toString(),
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const response = await fetch(`http://localhost:${server.port}`);
|
|
expect(response.status).toBe(200);
|
|
|
|
const text = await response.text();
|
|
expect(text).toBe("");
|
|
});
|