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 {...@@ -758,22 +758,14 @@ pub const BodyWriter = struct {
758 /// As a debugging utility, counts down to zero as bytes are written.758 /// As a debugging utility, counts down to zero as bytes are written.
759 content_length: u64,759 content_length: u64,
760 /// Each chunk is wrapped in a header and trailer.760 /// 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,
762 /// Cleanly finished stream; connection can be reused.765 /// Cleanly finished stream; connection can be reused.
763 end,766 end,
764767
765 pub const Chunked = union(enum) {768 pub const init_chunked: State = .{ .chunk_len = 0 };
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 };
777 };769 };
778770
779 pub fn isEliding(w: *const BodyWriter) bool {771 pub fn isEliding(w: *const BodyWriter) bool {
...@@ -784,21 +776,7 @@ pub const BodyWriter = struct {...@@ -784,21 +776,7 @@ pub const BodyWriter = struct {
784 pub fn flush(w: *BodyWriter) Error!void {776 pub fn flush(w: *BodyWriter) Error!void {
785 const out = w.http_protocol_output;777 const out = w.http_protocol_output;
786 switch (w.state) {778 switch (w.state) {
787 .end, .none, .content_length => return out.flush(),779 .end, .none, .content_length, .chunk_len => 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 },
802 }780 }
803 }781 }
804782
...@@ -841,7 +819,7 @@ pub const BodyWriter = struct {...@@ -841,7 +819,7 @@ pub const BodyWriter = struct {
841 w.state = .end;819 w.state = .end;
842 },820 },
843 .none => {},821 .none => {},
844 .chunked => return endChunkedUnflushed(w, .{}),822 .chunk_len => return endChunkedUnflushed(w, .{}),
845 }823 }
846 }824 }
847825
...@@ -877,24 +855,16 @@ pub const BodyWriter = struct {...@@ -877,24 +855,16 @@ pub const BodyWriter = struct {
877 /// * `endUnflushed`855 /// * `endUnflushed`
878 /// * `end`856 /// * `end`
879 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {857 pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void {
880 const chunked = &w.state.chunked;
881 if (w.isEliding()) {858 if (w.isEliding()) {
882 w.state = .end;859 w.state = .end;
883 return;860 return;
884 }861 }
885 const bw = w.http_protocol_output;862 const bw = w.http_protocol_output;
886 switch (chunked.*) {863 switch (w.state.chunk_len) {
887 .offset => |offset| {864 0 => {},
888 const chunk_len = bw.end - offset - chunk_header_template.len;865 1 => try bw.writeByte('\n'),
889 writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len);866 2 => try bw.writeAll("\r\n"),
890 try bw.writeAll("\r\n");867 else => unreachable, // An earlier write call indicated more data would follow.
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 },
898 }868 }
899 try bw.writeAll("0\r\n");869 try bw.writeAll("0\r\n");
900 for (options.trailers) |trailer| {870 for (options.trailers) |trailer| {
...@@ -991,44 +961,32 @@ pub const BodyWriter = struct {...@@ -991,44 +961,32 @@ pub const BodyWriter = struct {
991 return error.Unimplemented;961 return error.Unimplemented;
992 };962 };
993 const out = bw.http_protocol_output;963 const out = bw.http_protocol_output;
994 const chunked = &bw.state.chunked;964 switch (bw.state.chunk_len) {
995 state: switch (chunked.*) {965 0 => {
996 .offset => |off| {966 const header_buf = try out.writableArray(chunk_header_template.len);
997 // TODO: is it better perf to read small files into the buffer?967 @memcpy(header_buf, chunk_header_template);
998 const buffered_len = out.end - off - chunk_header_template.len;968 writeHex(header_buf[0..chunk_len_digits], data_len);
999 const chunk_len = data_len + buffered_len;
1000 writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len);
1001 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);969 const n = try out.sendFileHeader(w.buffered(), file_reader, limit);
1002 chunked.* = .{ .chunk_len = data_len + 2 - n };970 bw.state.chunk_len = data_len + 2 - n;
1003 return w.consume(n);971 const ret = w.consume(n);
972 return ret;
1004 },973 },
1005 .chunk_len => |chunk_len| l: switch (chunk_len) {974 1 => unreachable,
1006 0 => {975 2 => {
1007 const off = out.end;976 try out.writeAll("\r\n");
1008 const header_buf = try out.writableArray(chunk_header_template.len);977 bw.state.chunk_len = 0;
1009 @memcpy(header_buf, chunk_header_template);978 assert(file_reader.atEnd());
1010 chunked.* = .{ .offset = off };979 return error.EndOfStream;
1011 continue :state .{ .offset = off };980 },
1012 },981 else => {
1013 1 => {982 const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2);
1014 try out.writeByte('\n');983 const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit|
1015 chunked.chunk_len = 0;984 try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit))
1016 continue :l 0;985 else
1017 },986 try out.write(chunk_limit.slice(w.buffered()));
1018 2 => {987 bw.state.chunk_len -= n;
1019 try out.writeByte('\r');988 const ret = w.consume(n);
1020 chunked.chunk_len = 1;989 return ret;
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 },
1032 },990 },
1033 }991 }
1034 }992 }
...@@ -1038,42 +996,25 @@ pub const BodyWriter = struct {...@@ -1038,42 +996,25 @@ pub const BodyWriter = struct {
1038 assert(!bw.isEliding());996 assert(!bw.isEliding());
1039 const out = bw.http_protocol_output;997 const out = bw.http_protocol_output;
1040 const data_len = w.end + Writer.countSplat(data, splat);998 const data_len = w.end + Writer.countSplat(data, splat);
1041 const chunked = &bw.state.chunked;999 l: switch (bw.state.chunk_len) {
1042 state: switch (chunked.*) {1000 0 => {
1043 .offset => |offset| {1001 const header_buf = try out.writableArray(chunk_header_template.len);
1044 if (out.unusedCapacityLen() >= data_len) {1002 @memcpy(header_buf, chunk_header_template);
1045 return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable);1003 writeHex(header_buf[0..chunk_len_digits], data_len);
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);
1050 const n = try out.writeSplatHeader(w.buffered(), data, splat);1004 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;
1052 return w.consume(n);1006 return w.consume(n);
1053 },1007 },
1054 .chunk_len => |chunk_len| l: switch (chunk_len) {1008 1 => unreachable,
1055 0 => {1009 2 => {
1056 const offset = out.end;1010 try out.writeAll("\r\n");
1057 const header_buf = try out.writableArray(chunk_header_template.len);1011 bw.state.chunk_len = 0;
1058 @memcpy(header_buf, chunk_header_template);1012 continue :l 0;
1059 chunked.* = .{ .offset = offset };1013 },
1060 continue :state .{ .offset = offset };1014 else => {
1061 },1015 const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(bw.state.chunk_len - 2));
1062 1 => {1016 bw.state.chunk_len -= n;
1063 try out.writeByte('\n');1017 return w.consume(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 },
1077 },1018 },
1078 }1019 }
1079 }1020 }
lib/std/http/Client.zig+1-1
...@@ -907,7 +907,7 @@ pub const Request = struct {...@@ -907,7 +907,7 @@ pub const Request = struct {
907 return switch (r.transfer_encoding) {907 return switch (r.transfer_encoding) {
908 .chunked => .{908 .chunked => .{
909 .http_protocol_output = http_protocol_output,909 .http_protocol_output = http_protocol_output,
910 .state = .{ .chunked = .init },910 .state = .init_chunked,
911 .writer = .{911 .writer = .{
912 .buffer = buffer,912 .buffer = buffer,
913 .vtable = &.{913 .vtable = &.{
lib/std/http/Server.zig+3-3
...@@ -448,11 +448,11 @@ pub const Request = struct {...@@ -448,11 +448,11 @@ pub const Request = struct {
448 try out.writeAll("\r\n");448 try out.writeAll("\r\n");
449 const elide_body = request.head.method == .HEAD;449 const elide_body = request.head.method == .HEAD;
450 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {450 const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) {
451 .chunked => .{ .chunked = .init },451 .chunked => .init_chunked,
452 .none => .none,452 .none => .none,
453 } else if (options.content_length) |len| .{453 } else if (options.content_length) |len| .{
454 .content_length = len,454 .content_length = len,
455 } else .{ .chunked = .init };455 } else .init_chunked;
456456
457 return if (elide_body) .{457 return if (elide_body) .{
458 .http_protocol_output = request.server.out,458 .http_protocol_output = request.server.out,
...@@ -478,7 +478,7 @@ pub const Request = struct {...@@ -478,7 +478,7 @@ pub const Request = struct {
478 .drain = http.BodyWriter.contentLengthDrain,478 .drain = http.BodyWriter.contentLengthDrain,
479 .sendFile = http.BodyWriter.contentLengthSendFile,479 .sendFile = http.BodyWriter.contentLengthSendFile,
480 },480 },
481 .chunked => &.{481 .chunk_len => &.{
482 .drain = http.BodyWriter.chunkedDrain,482 .drain = http.BodyWriter.chunkedDrain,
483 .sendFile = http.BodyWriter.chunkedSendFile,483 .sendFile = http.BodyWriter.chunkedSendFile,
484 },484 },