authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-16 16:24:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-08-16 16:24:11-07:00
log623290ea9b23780303e18f200fb0e4ca1861cf57
treeacc22088dfed1591332374afcd0f7315310881b0
parent551e009da7be279293fe261b729280d29fcf81b0
parent0cfd07bc865efa4b74d32b16b7dfd1a68167ce14
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #24864 from ifreund/fix-std-cmd

http.BodyWriter: handle EOF in chunkedSendFile, simplify

4 files changed, 69 insertions(+), 118 deletions(-)

lib/compiler/std-docs.zig+13-3
...@@ -106,9 +106,19 @@ fn accept(context: *Context, connection: std.net.Server.Connection) void {...@@ -106,9 +106,19 @@ fn accept(context: *Context, connection: std.net.Server.Connection) void {
106 return;106 return;
107 },107 },
108 };108 };
109 serveRequest(&request, context) catch |err| {109 serveRequest(&request, context) catch |err| switch (err) {
110 std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) });110 error.WriteFailed => {
111 return;111 if (conn_writer.err) |e| {
112 std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
113 } else {
114 std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) });
115 }
116 return;
117 },
118 else => {
119 std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) });
120 return;
121 },
112 };122 };
113 }123 }
114}124}
lib/std/http.zig+52-111
...@@ -760,22 +760,14 @@ pub const BodyWriter = struct {...@@ -760,22 +760,14 @@ pub const BodyWriter = struct {
760 /// As a debugging utility, counts down to zero as bytes are written.760 /// As a debugging utility, counts down to zero as bytes are written.
761 content_length: u64,761 content_length: u64,
762 /// Each chunk is wrapped in a header and trailer.762 /// Each chunk is wrapped in a header and trailer.
763 chunked: Chunked,763 /// This length is the the number of bytes to be written before the
764 /// next header. This includes +2 for the `\r\n` trailer and is zero
765 /// for the beginning of the stream.
766 chunk_len: usize,
764 /// Cleanly finished stream; connection can be reused.767 /// Cleanly finished stream; connection can be reused.
765 end,768 end,
766769
767 pub const Chunked = union(enum) {770 pub const init_chunked: State = .{ .chunk_len = 0 };
768 /// Index to the start of the hex-encoded chunk length in the chunk
769 /// header within the buffer of `BodyWriter.http_protocol_output`.
770 /// Buffered chunk data starts here plus length of `chunk_header_template`.
771 offset: usize,
772 /// We are in the middle of a chunk and this is how many bytes are
773 /// left until the next header. This includes +2 for "\r"\n", and
774 /// is zero for the beginning of the stream.
775 chunk_len: usize,
776
777 pub const init: Chunked = .{ .chunk_len = 0 };
778 };
779 };771 };
780772
781 pub fn isEliding(w: *const BodyWriter) bool {773 pub fn isEliding(w: *const BodyWriter) bool {
...@@ -786,21 +778,7 @@ pub const BodyWriter = struct {...@@ -786,21 +778,7 @@ pub const BodyWriter = struct {
786 pub fn flush(w: *BodyWriter) Error!void {778 pub fn flush(w: *BodyWriter) Error!void {
787 const out = w.http_protocol_output;779 const out = w.http_protocol_output;
788 switch (w.state) {780 switch (w.state) {
789 .end, .none, .content_length => return out.flush(),781 .end, .none, .content_length, .chunk_len => return out.flush(),
790 .chunked => |*chunked| switch (chunked.*) {
791 .offset => |offset| {
792 const chunk_len = out.end - offset - chunk_header_template.len;
793 if (chunk_len > 0) {
794 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
795 chunked.* = .{ .chunk_len = 2 };
796 } else {
797 out.end = offset;
798 chunked.* = .{ .chunk_len = 0 };
799 }
800 try out.flush();
801 },
802 .chunk_len => return out.flush(),
803 },
804 }782 }
805 }783 }
806784
...@@ -843,7 +821,7 @@ pub const BodyWriter = struct {...@@ -843,7 +821,7 @@ pub const BodyWriter = struct {
843 w.state = .end;821 w.state = .end;
844 },822 },
845 .none => {},823 .none => {},
846 .chunked => return endChunkedUnflushed(w, .{}),824 .chunk_len => return endChunkedUnflushed(w, .{}),
847 }825 }
848 }826 }
849827
...@@ -879,24 +857,16 @@ pub const BodyWriter = struct {...@@ -879,24 +857,16 @@ pub const BodyWriter = struct {
879 /// * `endUnflushed`857 /// * `endUnflushed`
880 /// * `end`858 /// * `end`
881 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {859 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {
882 const chunked = &w.state.chunked;
883 if (w.isEliding()) {860 if (w.isEliding()) {
884 w.state = .end;861 w.state = .end;
885 return;862 return;
886 }863 }
887 const bw = w.http_protocol_output;864 const bw = w.http_protocol_output;
888 switch (chunked.*) {865 switch (w.state.chunk_len) {
889 .offset => |offset| {866 0 => {},
890 const chunk_len = bw.end - offset - chunk_header_template.len;867 1 => try bw.writeByte('\n'),
891 writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len);868 2 => try bw.writeAll("\r\n"),
892 try bw.writeAll("\r\n");869 else => unreachable, // An earlier write call indicated more data would follow.
893 },
894 .chunk_len => |chunk_len| switch (chunk_len) {
895 0 => {},
896 1 => try bw.writeByte('\n'),
897 2 => try bw.writeAll("\r\n"),
898 else => unreachable, // An earlier write call indicated more data would follow.
899 },
900 }870 }
901 try bw.writeAll("0\r\n");871 try bw.writeAll("0\r\n");
902 for (options.trailers) |trailer| {872 for (options.trailers) |trailer| {
...@@ -993,44 +963,32 @@ pub const BodyWriter = struct {...@@ -993,44 +963,32 @@ pub const BodyWriter = struct {
993 return error.Unimplemented;963 return error.Unimplemented;
994 };964 };
995 const out = bw.http_protocol_output;965 const out = bw.http_protocol_output;
996 const chunked = &bw.state.chunked;966 switch (bw.state.chunk_len) {
997 state: switch (chunked.*) {967 0 => {
998 .offset => |off| {968 const header_buf = try out.writableArray(chunk_header_template.len);
999 // TODO: is it better perf to read small files into the buffer?969 @memcpy(header_buf, chunk_header_template);
1000 const buffered_len = out.end - off - chunk_header_template.len;970 writeHex(header_buf[0..chunk_len_digits], data_len);
1001 const chunk_len = data_len + buffered_len;
1002 writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len);
1003 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);971 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
1004 chunked.* = .{ .chunk_len = data_len + 2 - n };972 bw.state.chunk_len = data_len + 2 - n;
1005 return w.consume(n);973 const ret = w.consume(n);
974 return ret;
1006 },975 },
1007 .chunk_len => |chunk_len| l: switch (chunk_len) {976 1 => unreachable,
1008 0 => {977 2 => {
1009 const off = out.end;978 try out.writeAll("\r\n");
1010 const header_buf = try out.writableArray(chunk_header_template.len);979 bw.state.chunk_len = 0;
1011 @memcpy(header_buf, chunk_header_template);980 assert(file_reader.atEnd());
1012 chunked.* = .{ .offset = off };981 return error.EndOfStream;
1013 continue :state .{ .offset = off };982 },
1014 },983 else => {
1015 1 => {984 const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2);
1016 try out.writeByte('\n');985 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit|
1017 chunked.chunk_len = 0;986 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit))
1018 continue :l 0;987 else
1019 },988 try out.write(chunk_limit.slice(w.buffered()));
1020 2 => {989 bw.state.chunk_len -= n;
1021 try out.writeByte('\r');990 const ret = w.consume(n);
1022 chunked.chunk_len = 1;991 return ret;
1023 continue :l 1;
1024 },
1025 else => {
1026 const chunk_limit: std.Io.Limit = .limited(chunk_len - 2);
1027 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit|
1028 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit))
1029 else
1030 try out.write(chunk_limit.slice(w.buffered()));
1031 chunked.chunk_len = chunk_len - n;
1032 return w.consume(n);
1033 },
1034 },992 },
1035 }993 }
1036 }994 }
...@@ -1040,42 +998,25 @@ pub const BodyWriter = struct {...@@ -1040,42 +998,25 @@ pub const BodyWriter = struct {
1040 assert(!bw.isEliding());998 assert(!bw.isEliding());
1041 const out = bw.http_protocol_output;999 const out = bw.http_protocol_output;
1042 const data_len = w.end + Writer.countSplat(data, splat);1000 const data_len = w.end + Writer.countSplat(data, splat);
1043 const chunked = &bw.state.chunked;1001 l: switch (bw.state.chunk_len) {
1044 state: switch (chunked.*) {1002 0 => {
1045 .offset => |offset| {1003 const header_buf = try out.writableArray(chunk_header_template.len);
1046 if (out.unusedCapacityLen() >= data_len) {1004 @memcpy(header_buf, chunk_header_template);
1047 return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable);1005 writeHex(header_buf[0..chunk_len_digits], data_len);
1048 }
1049 const buffered_len = out.end - offset - chunk_header_template.len;
1050 const chunk_len = data_len + buffered_len;
1051 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
1052 const n = try out.writeSplatHeader(w.buffered(), data, splat);1006 const n = try out.writeSplatHeader(w.buffered(), data, splat);
1053 chunked.* = .{ .chunk_len = data_len + 2 - n };1007 bw.state.chunk_len = data_len + 2 - n;
1054 return w.consume(n);1008 return w.consume(n);
1055 },1009 },
1056 .chunk_len => |chunk_len| l: switch (chunk_len) {1010 1 => unreachable,
1057 0 => {1011 2 => {
1058 const offset = out.end;1012 try out.writeAll("\r\n");
1059 const header_buf = try out.writableArray(chunk_header_template.len);1013 bw.state.chunk_len = 0;
1060 @memcpy(header_buf, chunk_header_template);1014 continue :l 0;
1061 chunked.* = .{ .offset = offset };1015 },
1062 continue :state .{ .offset = offset };1016 else => {
1063 },1017 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(bw.state.chunk_len - 2));
1064 1 => {1018 bw.state.chunk_len -= n;
1065 try out.writeByte('\n');1019 return w.consume(n);
1066 chunked.chunk_len = 0;
1067 continue :l 0;
1068 },
1069 2 => {
1070 try out.writeByte('\r');
1071 chunked.chunk_len = 1;
1072 continue :l 1;
1073 },
1074 else => {
1075 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2));
1076 chunked.chunk_len = chunk_len - n;
1077 return w.consume(n);
1078 },
1079 },1020 },
1080 }1021 }
1081 }1022 }
lib/std/http/Client.zig+1-1
...@@ -912,7 +912,7 @@ pub const Request = struct {...@@ -912,7 +912,7 @@ pub const Request = struct {
912 return switch (r.transfer_encoding) {912 return switch (r.transfer_encoding) {
913 .chunked => .{913 .chunked => .{
914 .http_protocol_output = http_protocol_output,914 .http_protocol_output = http_protocol_output,
915 .state = .{ .chunked = .init },915 .state = .init_chunked,
916 .writer = .{916 .writer = .{
917 .buffer = buffer,917 .buffer = buffer,
918 .vtable = &.{918 .vtable = &.{
lib/std/http/Server.zig+3-3
...@@ -450,11 +450,11 @@ pub const Request = struct {...@@ -450,11 +450,11 @@ pub const Request = struct {
450 try out.writeAll("\r\n");450 try out.writeAll("\r\n");
451 const elide_body = request.head.method == .HEAD;451 const elide_body = request.head.method == .HEAD;
452 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {452 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {
453 .chunked => .{ .chunked = .init },453 .chunked => .init_chunked,
454 .none => .none,454 .none => .none,
455 } else if (options.content_length) |len| .{455 } else if (options.content_length) |len| .{
456 .content_length = len,456 .content_length = len,
457 } else .{ .chunked = .init };457 } else .init_chunked;
458458
459 return if (elide_body) .{459 return if (elide_body) .{
460 .http_protocol_output = request.server.out,460 .http_protocol_output = request.server.out,
...@@ -480,7 +480,7 @@ pub const Request = struct {...@@ -480,7 +480,7 @@ pub const Request = struct {
480 .drain = http.BodyWriter.contentLengthDrain,480 .drain = http.BodyWriter.contentLengthDrain,
481 .sendFile = http.BodyWriter.contentLengthSendFile,481 .sendFile = http.BodyWriter.contentLengthSendFile,
482 },482 },
483 .chunked => &.{483 .chunk_len => &.{
484 .drain = http.BodyWriter.chunkedDrain,484 .drain = http.BodyWriter.chunkedDrain,
485 .sendFile = http.BodyWriter.chunkedSendFile,485 .sendFile = http.BodyWriter.chunkedSendFile,
486 },486 },