authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2025-08-16 13:11:19+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2025-08-16 22:07:16+02:00
log0cfd07bc865efa4b74d32b16b7dfd1a68167ce14
tree95e00d211eecf729491a4f2004834c95b04c3159
parentce4e8a991f4d3d6f8b3f1f987fb955af9238ab53
signaturelock-open Commit is signed but in an unrecognized format.

http.BodyWriter: handle EOF in chunkedSendFile, simplify

With these changes, the `zig std` command now works again and doesn't trigger assertion failures or mess up the chunked transfer encoding.

3 files changed, 56 insertions(+), 115 deletions(-)

lib/std/http.zig+52-111
......@@ -758,22 +758,14 @@ pub const BodyWriter = struct {
758758 /// As a debugging utility, counts down to zero as bytes are written.
759759 content_length: u64,
760760 /// Each chunk is wrapped in a header and trailer.
761 chunked: Chunked,
761 /// This length is the the number of bytes to be written before the
762 /// next header. This includes +2 for the `\r\n` trailer and is zero
763 /// for the beginning of the stream.
764 chunk_len: usize,
762765 /// Cleanly finished stream; connection can be reused.
763766 end,
764767
765 pub const Chunked = union(enum) {
766 /// Index to the start of the hex-encoded chunk length in the chunk
767 /// header within the buffer of `BodyWriter.http_protocol_output`.
768 /// Buffered chunk data starts here plus length of `chunk_header_template`.
769 offset: usize,
770 /// We are in the middle of a chunk and this is how many bytes are
771 /// left until the next header. This includes +2 for "\r"\n", and
772 /// is zero for the beginning of the stream.
773 chunk_len: usize,
774
775 pub const init: Chunked = .{ .chunk_len = 0 };
776 };
768 pub const init_chunked: State = .{ .chunk_len = 0 };
777769 };
778770
779771 pub fn isEliding(w: *const BodyWriter) bool {
......@@ -784,21 +776,7 @@ pub const BodyWriter = struct {
784776 pub fn flush(w: *BodyWriter) Error!void {
785777 const out = w.http_protocol_output;
786778 switch (w.state) {
787 .end, .none, .content_length => return out.flush(),
788 .chunked => |*chunked| switch (chunked.*) {
789 .offset => |offset| {
790 const chunk_len = out.end - offset - chunk_header_template.len;
791 if (chunk_len > 0) {
792 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
793 chunked.* = .{ .chunk_len = 2 };
794 } else {
795 out.end = offset;
796 chunked.* = .{ .chunk_len = 0 };
797 }
798 try out.flush();
799 },
800 .chunk_len => return out.flush(),
801 },
779 .end, .none, .content_length, .chunk_len => return out.flush(),
802780 }
803781 }
804782
......@@ -841,7 +819,7 @@ pub const BodyWriter = struct {
841819 w.state = .end;
842820 },
843821 .none => {},
844 .chunked => return endChunkedUnflushed(w, .{}),
822 .chunk_len => return endChunkedUnflushed(w, .{}),
845823 }
846824 }
847825
......@@ -877,24 +855,16 @@ pub const BodyWriter = struct {
877855 /// * `endUnflushed`
878856 /// * `end`
879857 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {
880 const chunked = &w.state.chunked;
881858 if (w.isEliding()) {
882859 w.state = .end;
883860 return;
884861 }
885862 const bw = w.http_protocol_output;
886 switch (chunked.*) {
887 .offset => |offset| {
888 const chunk_len = bw.end - offset - chunk_header_template.len;
889 writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len);
890 try bw.writeAll("\r\n");
891 },
892 .chunk_len => |chunk_len| switch (chunk_len) {
893 0 => {},
894 1 => try bw.writeByte('\n'),
895 2 => try bw.writeAll("\r\n"),
896 else => unreachable, // An earlier write call indicated more data would follow.
897 },
863 switch (w.state.chunk_len) {
864 0 => {},
865 1 => try bw.writeByte('\n'),
866 2 => try bw.writeAll("\r\n"),
867 else => unreachable, // An earlier write call indicated more data would follow.
898868 }
899869 try bw.writeAll("0\r\n");
900870 for (options.trailers) |trailer| {
......@@ -991,44 +961,32 @@ pub const BodyWriter = struct {
991961 return error.Unimplemented;
992962 };
993963 const out = bw.http_protocol_output;
994 const chunked = &bw.state.chunked;
995 state: switch (chunked.*) {
996 .offset => |off| {
997 // TODO: is it better perf to read small files into the buffer?
998 const buffered_len = out.end - off - chunk_header_template.len;
999 const chunk_len = data_len + buffered_len;
1000 writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len);
964 switch (bw.state.chunk_len) {
965 0 => {
966 const header_buf = try out.writableArray(chunk_header_template.len);
967 @memcpy(header_buf, chunk_header_template);
968 writeHex(header_buf[0..chunk_len_digits], data_len);
1001969 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
1002 chunked.* = .{ .chunk_len = data_len + 2 - n };
1003 return w.consume(n);
970 bw.state.chunk_len = data_len + 2 - n;
971 const ret = w.consume(n);
972 return ret;
1004973 },
1005 .chunk_len => |chunk_len| l: switch (chunk_len) {
1006 0 => {
1007 const off = out.end;
1008 const header_buf = try out.writableArray(chunk_header_template.len);
1009 @memcpy(header_buf, chunk_header_template);
1010 chunked.* = .{ .offset = off };
1011 continue :state .{ .offset = off };
1012 },
1013 1 => {
1014 try out.writeByte('\n');
1015 chunked.chunk_len = 0;
1016 continue :l 0;
1017 },
1018 2 => {
1019 try out.writeByte('\r');
1020 chunked.chunk_len = 1;
1021 continue :l 1;
1022 },
1023 else => {
1024 const chunk_limit: std.Io.Limit = .limited(chunk_len - 2);
1025 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit|
1026 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit))
1027 else
1028 try out.write(chunk_limit.slice(w.buffered()));
1029 chunked.chunk_len = chunk_len - n;
1030 return w.consume(n);
1031 },
974 1 => unreachable,
975 2 => {
976 try out.writeAll("\r\n");
977 bw.state.chunk_len = 0;
978 assert(file_reader.atEnd());
979 return error.EndOfStream;
980 },
981 else => {
982 const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2);
983 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit|
984 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit))
985 else
986 try out.write(chunk_limit.slice(w.buffered()));
987 bw.state.chunk_len -= n;
988 const ret = w.consume(n);
989 return ret;
1032990 },
1033991 }
1034992 }
......@@ -1038,42 +996,25 @@ pub const BodyWriter = struct {
1038996 assert(!bw.isEliding());
1039997 const out = bw.http_protocol_output;
1040998 const data_len = w.end + Writer.countSplat(data, splat);
1041 const chunked = &bw.state.chunked;
1042 state: switch (chunked.*) {
1043 .offset => |offset| {
1044 if (out.unusedCapacityLen() >= data_len) {
1045 return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable);
1046 }
1047 const buffered_len = out.end - offset - chunk_header_template.len;
1048 const chunk_len = data_len + buffered_len;
1049 writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len);
999 l: switch (bw.state.chunk_len) {
1000 0 => {
1001 const header_buf = try out.writableArray(chunk_header_template.len);
1002 @memcpy(header_buf, chunk_header_template);
1003 writeHex(header_buf[0..chunk_len_digits], data_len);
10501004 const n = try out.writeSplatHeader(w.buffered(), data, splat);
1051 chunked.* = .{ .chunk_len = data_len + 2 - n };
1005 bw.state.chunk_len = data_len + 2 - n;
10521006 return w.consume(n);
10531007 },
1054 .chunk_len => |chunk_len| l: switch (chunk_len) {
1055 0 => {
1056 const offset = out.end;
1057 const header_buf = try out.writableArray(chunk_header_template.len);
1058 @memcpy(header_buf, chunk_header_template);
1059 chunked.* = .{ .offset = offset };
1060 continue :state .{ .offset = offset };
1061 },
1062 1 => {
1063 try out.writeByte('\n');
1064 chunked.chunk_len = 0;
1065 continue :l 0;
1066 },
1067 2 => {
1068 try out.writeByte('\r');
1069 chunked.chunk_len = 1;
1070 continue :l 1;
1071 },
1072 else => {
1073 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2));
1074 chunked.chunk_len = chunk_len - n;
1075 return w.consume(n);
1076 },
1008 1 => unreachable,
1009 2 => {
1010 try out.writeAll("\r\n");
1011 bw.state.chunk_len = 0;
1012 continue :l 0;
1013 },
1014 else => {
1015 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(bw.state.chunk_len - 2));
1016 bw.state.chunk_len -= n;
1017 return w.consume(n);
10771018 },
10781019 }
10791020 }
lib/std/http/Client.zig+1-1
......@@ -907,7 +907,7 @@ pub const Request = struct {
907907 return switch (r.transfer_encoding) {
908908 .chunked => .{
909909 .http_protocol_output = http_protocol_output,
910 .state = .{ .chunked = .init },
910 .state = .init_chunked,
911911 .writer = .{
912912 .buffer = buffer,
913913 .vtable = &.{
lib/std/http/Server.zig+3-3
......@@ -448,11 +448,11 @@ pub const Request = struct {
448448 try out.writeAll("\r\n");
449449 const elide_body = request.head.method == .HEAD;
450450 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {
451 .chunked => .{ .chunked = .init },
451 .chunked => .init_chunked,
452452 .none => .none,
453453 } else if (options.content_length) |len| .{
454454 .content_length = len,
455 } else .{ .chunked = .init };
455 } else .init_chunked;
456456
457457 return if (elide_body) .{
458458 .http_protocol_output = request.server.out,
......@@ -478,7 +478,7 @@ pub const Request = struct {
478478 .drain = http.BodyWriter.contentLengthDrain,
479479 .sendFile = http.BodyWriter.contentLengthSendFile,
480480 },
481 .chunked => &.{
481 .chunk_len => &.{
482482 .drain = http.BodyWriter.chunkedDrain,
483483 .sendFile = http.BodyWriter.chunkedSendFile,
484484 },