authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-22 18:36:40-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-02-23 02:37:11-07:00
logd051b1396358d1b9229aefeeb2a61f7f46a4e5c3
tree37b135fef58da32828f06278a8c8d5d55a0ae553
parent737e7be46c825363a34f410cd3e4ea3045b3fce1

std.http.Server: implement respondStreaming with unknown len

no content-length header no transfer-encoding header

2 files changed, 116 insertions(+), 21 deletions(-)

lib/std/http/Server.zig+50-21
...@@ -474,7 +474,10 @@ pub const Request = struct {...@@ -474,7 +474,10 @@ pub const Request = struct {
474 }) catch unreachable;474 }) catch unreachable;
475 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");475 if (keep_alive) h.appendSliceAssumeCapacity("connection: keep-alive\r\n");
476476
477 if (options.content_length) |len| {477 if (o.transfer_encoding) |transfer_encoding| switch (transfer_encoding) {
478 .chunked => h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n"),
479 .none => {},
480 } else if (options.content_length) |len| {
478 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;481 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;
479 } else {482 } else {
480 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");483 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
...@@ -496,7 +499,12 @@ pub const Request = struct {...@@ -496,7 +499,12 @@ pub const Request = struct {
496 .send_buffer = options.send_buffer,499 .send_buffer = options.send_buffer,
497 .send_buffer_start = 0,500 .send_buffer_start = 0,
498 .send_buffer_end = h.items.len,501 .send_buffer_end = h.items.len,
499 .content_length = options.content_length,502 .transfer_encoding = if (o.transfer_encoding) |te| switch (te) {
503 .chunked => .chunked,
504 .none => .none,
505 } else if (options.content_length) |len| .{
506 .content_length = len,
507 } else .chunked,
500 .elide_body = elide_body,508 .elide_body = elide_body,
501 .chunk_len = 0,509 .chunk_len = 0,
502 };510 };
...@@ -709,12 +717,21 @@ pub const Response = struct {...@@ -709,12 +717,21 @@ pub const Response = struct {
709 send_buffer_end: usize,717 send_buffer_end: usize,
710 /// `null` means transfer-encoding: chunked.718 /// `null` means transfer-encoding: chunked.
711 /// As a debugging utility, counts down to zero as bytes are written.719 /// As a debugging utility, counts down to zero as bytes are written.
712 content_length: ?u64,720 transfer_encoding: TransferEncoding,
713 elide_body: bool,721 elide_body: bool,
714 /// Indicates how much of the end of the `send_buffer` corresponds to a722 /// Indicates how much of the end of the `send_buffer` corresponds to a
715 /// chunk. This amount of data will be wrapped by an HTTP chunk header.723 /// chunk. This amount of data will be wrapped by an HTTP chunk header.
716 chunk_len: usize,724 chunk_len: usize,
717725
726 pub const TransferEncoding = union(enum) {
727 /// End of connection signals the end of the stream.
728 none,
729 /// As a debugging utility, counts down to zero as bytes are written.
730 content_length: u64,
731 /// Each chunk is wrapped in a header and trailer.
732 chunked,
733 };
734
718 pub const WriteError = net.Stream.WriteError;735 pub const WriteError = net.Stream.WriteError;
719736
720 /// When using content-length, asserts that the amount of data sent matches737 /// When using content-length, asserts that the amount of data sent matches
...@@ -723,11 +740,17 @@ pub const Response = struct {...@@ -723,11 +740,17 @@ pub const Response = struct {
723 /// end-of-stream message, then flushes the stream to the system.740 /// end-of-stream message, then flushes the stream to the system.
724 /// Respects the value of `elide_body` to omit all data after the headers.741 /// Respects the value of `elide_body` to omit all data after the headers.
725 pub fn end(r: *Response) WriteError!void {742 pub fn end(r: *Response) WriteError!void {
726 if (r.content_length) |len| {743 switch (r.transfer_encoding) {
727 assert(len == 0); // Trips when end() called before all bytes written.744 .content_length => |len| {
728 try flush_cl(r);745 assert(len == 0); // Trips when end() called before all bytes written.
729 } else {746 try flush_cl(r);
730 try flush_chunked(r, &.{});747 },
748 .none => {
749 try flush_cl(r);
750 },
751 .chunked => {
752 try flush_chunked(r, &.{});
753 },
731 }754 }
732 r.* = undefined;755 r.* = undefined;
733 }756 }
...@@ -752,16 +775,21 @@ pub const Response = struct {...@@ -752,16 +775,21 @@ pub const Response = struct {
752 /// May return 0, which does not indicate end of stream. The caller decides775 /// May return 0, which does not indicate end of stream. The caller decides
753 /// when the end of stream occurs by calling `end`.776 /// when the end of stream occurs by calling `end`.
754 pub fn write(r: *Response, bytes: []const u8) WriteError!usize {777 pub fn write(r: *Response, bytes: []const u8) WriteError!usize {
755 if (r.content_length != null) {778 switch (r.transfer_encoding) {
756 return write_cl(r, bytes);779 .content_length, .none => return write_cl(r, bytes),
757 } else {780 .chunked => return write_chunked(r, bytes),
758 return write_chunked(r, bytes);
759 }781 }
760 }782 }
761783
762 fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {784 fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {
763 const r: *Response = @constCast(@alignCast(@ptrCast(context)));785 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
764 const len = &r.content_length.?;786
787 var trash: u64 = std.math.maxInt(u64);
788 const len = switch (r.transfer_encoding) {
789 .content_length => |*len| len,
790 else => &trash,
791 };
792
765 if (r.elide_body) {793 if (r.elide_body) {
766 len.* -= bytes.len;794 len.* -= bytes.len;
767 return bytes.len;795 return bytes.len;
...@@ -805,7 +833,7 @@ pub const Response = struct {...@@ -805,7 +833,7 @@ pub const Response = struct {
805833
806 fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {834 fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {
807 const r: *Response = @constCast(@alignCast(@ptrCast(context)));835 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
808 assert(r.content_length == null);836 assert(r.transfer_encoding == .chunked);
809837
810 if (r.elide_body)838 if (r.elide_body)
811 return bytes.len;839 return bytes.len;
...@@ -867,15 +895,13 @@ pub const Response = struct {...@@ -867,15 +895,13 @@ pub const Response = struct {
867 /// This is redundant after calling `end`.895 /// This is redundant after calling `end`.
868 /// Respects the value of `elide_body` to omit all data after the headers.896 /// Respects the value of `elide_body` to omit all data after the headers.
869 pub fn flush(r: *Response) WriteError!void {897 pub fn flush(r: *Response) WriteError!void {
870 if (r.content_length != null) {898 switch (r.transfer_encoding) {
871 return flush_cl(r);899 .none, .content_length => return flush_cl(r),
872 } else {900 .chunked => return flush_chunked(r, null),
873 return flush_chunked(r, null);
874 }901 }
875 }902 }
876903
877 fn flush_cl(r: *Response) WriteError!void {904 fn flush_cl(r: *Response) WriteError!void {
878 assert(r.content_length != null);
879 try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);905 try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);
880 r.send_buffer_start = 0;906 r.send_buffer_start = 0;
881 r.send_buffer_end = 0;907 r.send_buffer_end = 0;
...@@ -884,7 +910,7 @@ pub const Response = struct {...@@ -884,7 +910,7 @@ pub const Response = struct {
884 fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {910 fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {
885 const max_trailers = 25;911 const max_trailers = 25;
886 if (end_trailers) |trailers| assert(trailers.len <= max_trailers);912 if (end_trailers) |trailers| assert(trailers.len <= max_trailers);
887 assert(r.content_length == null);913 assert(r.transfer_encoding == .chunked);
888914
889 const http_headers = r.send_buffer[r.send_buffer_start .. r.send_buffer_end - r.chunk_len];915 const http_headers = r.send_buffer[r.send_buffer_start .. r.send_buffer_end - r.chunk_len];
890916
...@@ -976,7 +1002,10 @@ pub const Response = struct {...@@ -976,7 +1002,10 @@ pub const Response = struct {
9761002
977 pub fn writer(r: *Response) std.io.AnyWriter {1003 pub fn writer(r: *Response) std.io.AnyWriter {
978 return .{1004 return .{
979 .writeFn = if (r.content_length != null) write_cl else write_chunked,1005 .writeFn = switch (r.transfer_encoding) {
1006 .none, .content_length => write_cl,
1007 .chunked => write_chunked,
1008 },
980 .context = r,1009 .context = r,
981 };1010 };
982 }1011 }
lib/std/http/test.zig+66
...@@ -222,6 +222,72 @@ test "echo content server" {...@@ -222,6 +222,72 @@ test "echo content server" {
222 }222 }
223}223}
224224
225test "Server.Request.respondStreaming non-chunked, unknown content-length" {
226 // In this case, the response is expected to stream until the connection is
227 // closed, indicating the end of the body.
228 const test_server = try createTestServer(struct {
229 fn run(net_server: *std.net.Server) anyerror!void {
230 var header_buffer: [1000]u8 = undefined;
231 var remaining: usize = 1;
232 while (remaining != 0) : (remaining -= 1) {
233 const conn = try net_server.accept();
234 defer conn.stream.close();
235
236 var server = std.http.Server.init(conn, &header_buffer);
237
238 try expectEqual(.ready, server.state);
239 var request = try server.receiveHead();
240 try expectEqualStrings(request.head.target, "/foo");
241 var send_buffer: [500]u8 = undefined;
242 var response = request.respondStreaming(.{
243 .send_buffer = &send_buffer,
244 .respond_options = .{
245 .transfer_encoding = .none,
246 },
247 });
248 var total: usize = 0;
249 for (0..500) |i| {
250 var buf: [30]u8 = undefined;
251 const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
252 try response.writeAll(line);
253 total += line.len;
254 }
255 try expectEqual(7390, total);
256 try response.end();
257 try expectEqual(.closing, server.state);
258 }
259 }
260 });
261 defer test_server.destroy();
262
263 const request_bytes = "GET /foo HTTP/1.1\r\n\r\n";
264 const gpa = std.testing.allocator;
265 const stream = try std.net.tcpConnectToHost(gpa, "127.0.0.1", test_server.port());
266 defer stream.close();
267 try stream.writeAll(request_bytes);
268
269 const response = try stream.reader().readAllAlloc(gpa, 8192);
270 defer gpa.free(response);
271
272 var expected_response = std.ArrayList(u8).init(gpa);
273 defer expected_response.deinit();
274
275 try expected_response.appendSlice("HTTP/1.1 200 OK\r\n\r\n");
276
277 {
278 var total: usize = 0;
279 for (0..500) |i| {
280 var buf: [30]u8 = undefined;
281 const line = try std.fmt.bufPrint(&buf, "{d}, ah ha ha!\n", .{i});
282 try expected_response.appendSlice(line);
283 total += line.len;
284 }
285 try expectEqual(7390, total);
286 }
287
288 try expectEqualStrings(expected_response.items, response);
289}
290
225fn echoTests(client: *std.http.Client, port: u16) !void {291fn echoTests(client: *std.http.Client, port: u16) !void {
226 const gpa = std.testing.allocator;292 const gpa = std.testing.allocator;
227 var location_buffer: [100]u8 = undefined;293 var location_buffer: [100]u8 = undefined;