mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +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>
123 lines
4.8 KiB
Zig
123 lines
4.8 KiB
Zig
pub const Decompressor = union(enum) {
|
|
zlib: *Zlib.ZlibReaderArrayList,
|
|
brotli: *Brotli.BrotliReaderArrayList,
|
|
zstd: *zstd.ZstdReaderArrayList,
|
|
none: void,
|
|
|
|
pub fn deinit(this: *Decompressor) void {
|
|
switch (this.*) {
|
|
inline .brotli, .zlib, .zstd => |that| {
|
|
that.deinit();
|
|
this.* = .{ .none = {} };
|
|
},
|
|
.none => {},
|
|
}
|
|
}
|
|
|
|
pub fn updateBuffers(this: *Decompressor, encoding: Encoding, buffer: []const u8, body_out_str: *MutableString) !void {
|
|
if (!encoding.isCompressed()) {
|
|
return;
|
|
}
|
|
|
|
if (this.* == .none) {
|
|
switch (encoding) {
|
|
.gzip, .deflate => {
|
|
this.* = .{
|
|
.zlib = try Zlib.ZlibReaderArrayList.initWithOptionsAndListAllocator(
|
|
buffer,
|
|
&body_out_str.list,
|
|
body_out_str.allocator,
|
|
bun.http.default_allocator,
|
|
.{
|
|
// zlib.MAX_WBITS = 15
|
|
// to (de-)compress deflate format, use wbits = -zlib.MAX_WBITS
|
|
// to (de-)compress deflate format with headers we use wbits = 0 (we can detect the first byte using 120)
|
|
// to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
|
|
.windowBits = if (encoding == Encoding.gzip) Zlib.MAX_WBITS | 16 else (if (buffer.len > 1 and buffer[0] == 120) 0 else -Zlib.MAX_WBITS),
|
|
},
|
|
),
|
|
};
|
|
return;
|
|
},
|
|
.brotli => {
|
|
this.* = .{
|
|
.brotli = try Brotli.BrotliReaderArrayList.newWithOptions(
|
|
buffer,
|
|
&body_out_str.list,
|
|
body_out_str.allocator,
|
|
.{},
|
|
),
|
|
};
|
|
return;
|
|
},
|
|
.zstd => {
|
|
this.* = .{
|
|
.zstd = try zstd.ZstdReaderArrayList.initWithListAllocator(
|
|
buffer,
|
|
&body_out_str.list,
|
|
body_out_str.allocator,
|
|
bun.http.default_allocator,
|
|
),
|
|
};
|
|
return;
|
|
},
|
|
else => @panic("Invalid encoding. This code should not be reachable"),
|
|
}
|
|
}
|
|
|
|
switch (this.*) {
|
|
.zlib => |reader| {
|
|
bun.assert(reader.zlib.avail_in == 0);
|
|
reader.zlib.next_in = buffer.ptr;
|
|
reader.zlib.avail_in = @as(u32, @truncate(buffer.len));
|
|
|
|
const initial = body_out_str.list.items.len;
|
|
body_out_str.list.expandToCapacity();
|
|
if (body_out_str.list.capacity == initial) {
|
|
try body_out_str.list.ensureUnusedCapacity(body_out_str.allocator, 4096);
|
|
body_out_str.list.expandToCapacity();
|
|
}
|
|
reader.list = body_out_str.list;
|
|
reader.zlib.next_out = @ptrCast(&body_out_str.list.items[initial]);
|
|
reader.zlib.avail_out = @as(u32, @truncate(body_out_str.list.capacity - initial));
|
|
// we reset the total out so we can track how much we decompressed this time
|
|
reader.zlib.total_out = @truncate(initial);
|
|
},
|
|
.brotli => |reader| {
|
|
reader.input = buffer;
|
|
reader.total_in = 0;
|
|
|
|
const initial = body_out_str.list.items.len;
|
|
reader.list = body_out_str.list;
|
|
reader.total_out = @truncate(initial);
|
|
},
|
|
.zstd => |reader| {
|
|
reader.input = buffer;
|
|
reader.total_in = 0;
|
|
|
|
const initial = body_out_str.list.items.len;
|
|
reader.list = body_out_str.list;
|
|
reader.total_out = @truncate(initial);
|
|
},
|
|
else => @panic("Invalid encoding. This code should not be reachable"),
|
|
}
|
|
}
|
|
|
|
pub fn readAll(this: *Decompressor, is_done: bool) !void {
|
|
switch (this.*) {
|
|
.zlib => |zlib| try zlib.readAll(is_done),
|
|
.brotli => |brotli| try brotli.readAll(is_done),
|
|
.zstd => |reader| try reader.readAll(is_done),
|
|
.none => {},
|
|
}
|
|
}
|
|
};
|
|
|
|
const Zlib = @import("../zlib.zig");
|
|
const Encoding = @import("./Encoding.zig").Encoding;
|
|
|
|
const bun = @import("bun");
|
|
const Brotli = bun.brotli;
|
|
const MutableString = bun.MutableString;
|
|
const zstd = bun.zstd;
|