From 0bbf0461d9800ce5eba28cd32ef10492b2f80639 Mon Sep 17 00:00:00 2001 From: Luna Schwalbe Date: Sun, 7 Dec 2025 04:28:01 +0100 Subject: [PATCH] std.http: reliably update reader state Content length based reading would only set the reader state to `ready` once it returned EOF, but wrapping readers (such as decompressors) may stop reading from the underlying source without receiving EOF. In such cases the http reader state would stay set to `body_remaining_content_length`, even though the entire body had been read. Fixes #30060 Co-authored-by: Andrew Kelley --- lib/std/http.zig | 24 +++++++++++++----------- lib/std/http/test.zig | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/std/http.zig b/lib/std/http.zig index 291e22539b312f8a4ca497e0686ca51e17162083..300ae397ecfc30d1536770f05f7a16b9207124ca 100644 --- a/lib/std/http.zig +++ b/lib/std/http.zig @@ -443,7 +443,7 @@ pub const Reader = struct { }, .none => { if (content_length) |len| { - reader.state = .{ .body_remaining_content_length = len }; + reader.state = if (len == 0) .ready else .{ .body_remaining_content_length = len }; reader.interface = .{ .buffer = transfer_buffer, .seek = 0, @@ -509,27 +509,29 @@ pub const Reader = struct { limit: std.Io.Limit, ) std.Io.Reader.StreamError!usize { const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); + if (reader.state == .ready) return error.EndOfStream; const remaining_content_length = &reader.state.body_remaining_content_length; const remaining = remaining_content_length.*; - if (remaining == 0) { - reader.state = .ready; - return error.EndOfStream; - } const n = try reader.in.stream(w, limit.min(.limited64(remaining))); - remaining_content_length.* = remaining - n; + if (n == remaining) { + reader.state = .ready; + } else { + remaining_content_length.* = remaining - n; + } return n; } fn contentLengthDiscard(io_r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize { const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_r)); + if (reader.state == .ready) return error.EndOfStream; const remaining_content_length = &reader.state.body_remaining_content_length; const remaining = remaining_content_length.*; - if (remaining == 0) { - reader.state = .ready; - return error.EndOfStream; - } const n = try reader.in.discard(limit.min(.limited64(remaining))); - remaining_content_length.* = remaining - n; + if (n == remaining) { + reader.state = .ready; + } else { + remaining_content_length.* = remaining - n; + } return n; } diff --git a/lib/std/http/test.zig b/lib/std/http/test.zig index b061f4b2ac20b2f16d1f50ac4f912f13dc4e3622..42fd9f8fc9f84913d74bfd4e990a1e2eb57fe9c3 100644 --- a/lib/std/http/test.zig +++ b/lib/std/http/test.zig @@ -11,6 +11,28 @@ const expectEqual = std.testing.expectEqual; const expectEqualStrings = std.testing.expectEqualStrings; const expectError = std.testing.expectError; +test "content length reader state update" { + var in = Io.Reader.fixed("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nHello!\r\nHTTP/1.1 200 OK\r\n\r\n"); + var reader: http.Reader = .{ + .in = &in, + .interface = undefined, + .state = .ready, + .max_head_len = 1024, + }; + + _ = try reader.receiveHead(); + var body: [6]u8 = undefined; + _ = try reader.bodyReader(&.{}, .none, body.len).readSliceAll(&body); + try expectEqual(.ready, reader.state); + _ = try reader.receiveHead(); + + in.seek = 0; + _ = try reader.receiveHead(); + try reader.bodyReader(&.{}, .none, body.len).discardAll(body.len); + try expectEqual(.ready, reader.state); + _ = try reader.receiveHead(); +} + test "trailers" { if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .Debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/171879 if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806 -- 2.54.0