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 {
474474 }) catch unreachable;
475475 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| {
478481 h.fixedWriter().print("content-length: {d}\r\n", .{len}) catch unreachable;
479482 } else {
480483 h.appendSliceAssumeCapacity("transfer-encoding: chunked\r\n");
......@@ -496,7 +499,12 @@ pub const Request = struct {
496499 .send_buffer = options.send_buffer,
497500 .send_buffer_start = 0,
498501 .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,
500508 .elide_body = elide_body,
501509 .chunk_len = 0,
502510 };
......@@ -709,12 +717,21 @@ pub const Response = struct {
709717 send_buffer_end: usize,
710718 /// `null` means transfer-encoding: chunked.
711719 /// As a debugging utility, counts down to zero as bytes are written.
712 content_length: ?u64,
720 transfer_encoding: TransferEncoding,
713721 elide_body: bool,
714722 /// Indicates how much of the end of the `send_buffer` corresponds to a
715723 /// chunk. This amount of data will be wrapped by an HTTP chunk header.
716724 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
718735 pub const WriteError = net.Stream.WriteError;
719736
720737 /// When using content-length, asserts that the amount of data sent matches
......@@ -723,11 +740,17 @@ pub const Response = struct {
723740 /// end-of-stream message, then flushes the stream to the system.
724741 /// Respects the value of `elide_body` to omit all data after the headers.
725742 pub fn end(r: *Response) WriteError!void {
726 if (r.content_length) |len| {
727 assert(len == 0); // Trips when end() called before all bytes written.
728 try flush_cl(r);
729 } else {
730 try flush_chunked(r, &.{});
743 switch (r.transfer_encoding) {
744 .content_length => |len| {
745 assert(len == 0); // Trips when end() called before all bytes written.
746 try flush_cl(r);
747 },
748 .none => {
749 try flush_cl(r);
750 },
751 .chunked => {
752 try flush_chunked(r, &.{});
753 },
731754 }
732755 r.* = undefined;
733756 }
......@@ -752,16 +775,21 @@ pub const Response = struct {
752775 /// May return 0, which does not indicate end of stream. The caller decides
753776 /// when the end of stream occurs by calling `end`.
754777 pub fn write(r: *Response, bytes: []const u8) WriteError!usize {
755 if (r.content_length != null) {
756 return write_cl(r, bytes);
757 } else {
758 return write_chunked(r, bytes);
778 switch (r.transfer_encoding) {
779 .content_length, .none => return write_cl(r, bytes),
780 .chunked => return write_chunked(r, bytes),
759781 }
760782 }
761783
762784 fn write_cl(context: *const anyopaque, bytes: []const u8) WriteError!usize {
763785 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
765793 if (r.elide_body) {
766794 len.* -= bytes.len;
767795 return bytes.len;
......@@ -805,7 +833,7 @@ pub const Response = struct {
805833
806834 fn write_chunked(context: *const anyopaque, bytes: []const u8) WriteError!usize {
807835 const r: *Response = @constCast(@alignCast(@ptrCast(context)));
808 assert(r.content_length == null);
836 assert(r.transfer_encoding == .chunked);
809837
810838 if (r.elide_body)
811839 return bytes.len;
......@@ -867,15 +895,13 @@ pub const Response = struct {
867895 /// This is redundant after calling `end`.
868896 /// Respects the value of `elide_body` to omit all data after the headers.
869897 pub fn flush(r: *Response) WriteError!void {
870 if (r.content_length != null) {
871 return flush_cl(r);
872 } else {
873 return flush_chunked(r, null);
898 switch (r.transfer_encoding) {
899 .none, .content_length => return flush_cl(r),
900 .chunked => return flush_chunked(r, null),
874901 }
875902 }
876903
877904 fn flush_cl(r: *Response) WriteError!void {
878 assert(r.content_length != null);
879905 try r.stream.writeAll(r.send_buffer[r.send_buffer_start..r.send_buffer_end]);
880906 r.send_buffer_start = 0;
881907 r.send_buffer_end = 0;
......@@ -884,7 +910,7 @@ pub const Response = struct {
884910 fn flush_chunked(r: *Response, end_trailers: ?[]const http.Header) WriteError!void {
885911 const max_trailers = 25;
886912 if (end_trailers) |trailers| assert(trailers.len <= max_trailers);
887 assert(r.content_length == null);
913 assert(r.transfer_encoding == .chunked);
888914
889915 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 {
9761002
9771003 pub fn writer(r: *Response) std.io.AnyWriter {
9781004 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 },
9801009 .context = r,
9811010 };
9821011 }
lib/std/http/test.zig+66
......@@ -222,6 +222,72 @@ test "echo content server" {
222222 }
223223}
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
225291fn echoTests(client: *std.http.Client, port: u16) !void {
226292 const gpa = std.testing.allocator;
227293 var location_buffer: [100]u8 = undefined;