-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
http.BodyWriter: handle EOF in chunkedSendFile, simplify #24864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -758,22 +758,14 @@ pub const BodyWriter = struct { | |
| /// As a debugging utility, counts down to zero as bytes are written. | ||
| content_length: u64, | ||
| /// Each chunk is wrapped in a header and trailer. | ||
| chunked: Chunked, | ||
| /// This length is the the number of bytes to be written before the | ||
| /// next header. This includes +2 for the `\r\n` trailer and is zero | ||
| /// for the beginning of the stream. | ||
| chunk_len: usize, | ||
| /// Cleanly finished stream; connection can be reused. | ||
| end, | ||
|
|
||
| pub const Chunked = union(enum) { | ||
| /// Index to the start of the hex-encoded chunk length in the chunk | ||
| /// header within the buffer of `BodyWriter.http_protocol_output`. | ||
| /// Buffered chunk data starts here plus length of `chunk_header_template`. | ||
| offset: usize, | ||
| /// We are in the middle of a chunk and this is how many bytes are | ||
| /// left until the next header. This includes +2 for "\r"\n", and | ||
| /// is zero for the beginning of the stream. | ||
| chunk_len: usize, | ||
|
|
||
| pub const init: Chunked = .{ .chunk_len = 0 }; | ||
| }; | ||
| pub const init_chunked: State = .{ .chunk_len = 0 }; | ||
| }; | ||
|
|
||
| pub fn isEliding(w: *const BodyWriter) bool { | ||
|
|
@@ -784,21 +776,7 @@ pub const BodyWriter = struct { | |
| pub fn flush(w: *BodyWriter) Error!void { | ||
| const out = w.http_protocol_output; | ||
| switch (w.state) { | ||
| .end, .none, .content_length => return out.flush(), | ||
| .chunked => |*chunked| switch (chunked.*) { | ||
| .offset => |offset| { | ||
| const chunk_len = out.end - offset - chunk_header_template.len; | ||
| if (chunk_len > 0) { | ||
| writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| chunked.* = .{ .chunk_len = 2 }; | ||
| } else { | ||
| out.end = offset; | ||
| chunked.* = .{ .chunk_len = 0 }; | ||
| } | ||
| try out.flush(); | ||
| }, | ||
| .chunk_len => return out.flush(), | ||
| }, | ||
| .end, .none, .content_length, .chunk_len => return out.flush(), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -841,7 +819,7 @@ pub const BodyWriter = struct { | |
| w.state = .end; | ||
| }, | ||
| .none => {}, | ||
| .chunked => return endChunkedUnflushed(w, .{}), | ||
| .chunk_len => return endChunkedUnflushed(w, .{}), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -877,24 +855,16 @@ pub const BodyWriter = struct { | |
| /// * `endUnflushed` | ||
| /// * `end` | ||
| pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { | ||
| const chunked = &w.state.chunked; | ||
| if (w.isEliding()) { | ||
| w.state = .end; | ||
| return; | ||
| } | ||
| const bw = w.http_protocol_output; | ||
| switch (chunked.*) { | ||
| .offset => |offset| { | ||
| const chunk_len = bw.end - offset - chunk_header_template.len; | ||
| writeHex(bw.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| try bw.writeAll("\r\n"); | ||
| }, | ||
| .chunk_len => |chunk_len| switch (chunk_len) { | ||
| 0 => {}, | ||
| 1 => try bw.writeByte('\n'), | ||
| 2 => try bw.writeAll("\r\n"), | ||
| else => unreachable, // An earlier write call indicated more data would follow. | ||
| }, | ||
| switch (w.state.chunk_len) { | ||
| 0 => {}, | ||
| 1 => try bw.writeByte('\n'), | ||
| 2 => try bw.writeAll("\r\n"), | ||
| else => unreachable, // An earlier write call indicated more data would follow. | ||
| } | ||
| try bw.writeAll("0\r\n"); | ||
| for (options.trailers) |trailer| { | ||
|
|
@@ -991,44 +961,32 @@ pub const BodyWriter = struct { | |
| return error.Unimplemented; | ||
| }; | ||
| const out = bw.http_protocol_output; | ||
| const chunked = &bw.state.chunked; | ||
| state: switch (chunked.*) { | ||
| .offset => |off| { | ||
| // TODO: is it better perf to read small files into the buffer? | ||
| const buffered_len = out.end - off - chunk_header_template.len; | ||
| const chunk_len = data_len + buffered_len; | ||
| writeHex(out.buffer[off..][0..chunk_len_digits], chunk_len); | ||
| switch (bw.state.chunk_len) { | ||
| 0 => { | ||
| const header_buf = try out.writableArray(chunk_header_template.len); | ||
| @memcpy(header_buf, chunk_header_template); | ||
| writeHex(header_buf[0..chunk_len_digits], data_len); | ||
| const n = try out.sendFileHeader(w.buffered(), file_reader, limit); | ||
| chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| return w.consume(n); | ||
| bw.state.chunk_len = data_len + 2 - n; | ||
| const ret = w.consume(n); | ||
| return ret; | ||
| }, | ||
| .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 0 => { | ||
| const off = out.end; | ||
| const header_buf = try out.writableArray(chunk_header_template.len); | ||
| @memcpy(header_buf, chunk_header_template); | ||
| chunked.* = .{ .offset = off }; | ||
| continue :state .{ .offset = off }; | ||
| }, | ||
| 1 => { | ||
| try out.writeByte('\n'); | ||
| chunked.chunk_len = 0; | ||
| continue :l 0; | ||
| }, | ||
| 2 => { | ||
| try out.writeByte('\r'); | ||
| chunked.chunk_len = 1; | ||
| continue :l 1; | ||
| }, | ||
| else => { | ||
| const chunk_limit: std.Io.Limit = .limited(chunk_len - 2); | ||
| const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit| | ||
| try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit)) | ||
| else | ||
| try out.write(chunk_limit.slice(w.buffered())); | ||
| chunked.chunk_len = chunk_len - n; | ||
| return w.consume(n); | ||
| }, | ||
| 1 => unreachable, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this really unreachable? it looks like it could be reached from line 970 setting chunk_len to 1, or line 987 setting chunk_len to 1.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it is unreachable. It is in fact an assertion that we do not violate the protocol by e.g. writing only
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opened #24884 to further improve the clarity of this code and document the assertions better. |
||
| 2 => { | ||
| try out.writeAll("\r\n"); | ||
| bw.state.chunk_len = 0; | ||
| assert(file_reader.atEnd()); | ||
| return error.EndOfStream; | ||
| }, | ||
| else => { | ||
| const chunk_limit: std.Io.Limit = .limited(bw.state.chunk_len - 2); | ||
| const n = if (chunk_limit.subtract(w.buffered().len)) |sendfile_limit| | ||
| try out.sendFileHeader(w.buffered(), file_reader, sendfile_limit.min(limit)) | ||
| else | ||
| try out.write(chunk_limit.slice(w.buffered())); | ||
| bw.state.chunk_len -= n; | ||
| const ret = w.consume(n); | ||
| return ret; | ||
| }, | ||
| } | ||
| } | ||
|
|
@@ -1038,42 +996,25 @@ pub const BodyWriter = struct { | |
| assert(!bw.isEliding()); | ||
| const out = bw.http_protocol_output; | ||
| const data_len = w.end + Writer.countSplat(data, splat); | ||
| const chunked = &bw.state.chunked; | ||
| state: switch (chunked.*) { | ||
| .offset => |offset| { | ||
| if (out.unusedCapacityLen() >= data_len) { | ||
| return w.consume(out.writeSplatHeader(w.buffered(), data, splat) catch unreachable); | ||
| } | ||
| const buffered_len = out.end - offset - chunk_header_template.len; | ||
| const chunk_len = data_len + buffered_len; | ||
| writeHex(out.buffer[offset..][0..chunk_len_digits], chunk_len); | ||
| l: switch (bw.state.chunk_len) { | ||
| 0 => { | ||
| const header_buf = try out.writableArray(chunk_header_template.len); | ||
| @memcpy(header_buf, chunk_header_template); | ||
| writeHex(header_buf[0..chunk_len_digits], data_len); | ||
| const n = try out.writeSplatHeader(w.buffered(), data, splat); | ||
| chunked.* = .{ .chunk_len = data_len + 2 - n }; | ||
| bw.state.chunk_len = data_len + 2 - n; | ||
| return w.consume(n); | ||
| }, | ||
| .chunk_len => |chunk_len| l: switch (chunk_len) { | ||
| 0 => { | ||
| const offset = out.end; | ||
| const header_buf = try out.writableArray(chunk_header_template.len); | ||
| @memcpy(header_buf, chunk_header_template); | ||
| chunked.* = .{ .offset = offset }; | ||
| continue :state .{ .offset = offset }; | ||
| }, | ||
| 1 => { | ||
| try out.writeByte('\n'); | ||
| chunked.chunk_len = 0; | ||
| continue :l 0; | ||
| }, | ||
| 2 => { | ||
| try out.writeByte('\r'); | ||
| chunked.chunk_len = 1; | ||
| continue :l 1; | ||
| }, | ||
| else => { | ||
| const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(chunk_len - 2)); | ||
| chunked.chunk_len = chunk_len - n; | ||
| return w.consume(n); | ||
| }, | ||
| 1 => unreachable, | ||
| 2 => { | ||
| try out.writeAll("\r\n"); | ||
| bw.state.chunk_len = 0; | ||
| continue :l 0; | ||
| }, | ||
| else => { | ||
| const n = try out.writeSplatHeaderLimit(w.buffered(), data, splat, .limited(bw.state.chunk_len - 2)); | ||
| bw.state.chunk_len -= n; | ||
| return w.consume(n); | ||
| }, | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to give you some background for why this was done: the idea was that when flushing a chunked http transfer request, it should finish the chunk and send the end-of-chunk trailer and start a new chunk. Looking at it with fresh eyes, I'm not sure how valuable that is, although I can think of some reasons for it.
anyway I think the simplification that you brought is nice, so let's run with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this offset code in flush was actually unreachable before my commit.
I don't think it's possible to implement what you describe either. We can't finish the chunk early without writing the full amount of data specified in the chunk header, and we don't have access to data that the user of the API started but did not finish writing here in flush().