| author | |
| committer | |
| log | 310a579efcaa4d7826276f07ca6cae501d841444 |
| tree | 5d5b83619105f54bd6d5962b4c5129fdb958d3ce |
| parent | faf8bbf5b184d2593d6b3a979c9d42921cd716c0 |
4 files changed, 201 insertions(+), 30 deletions(-)
lib/std/http.zig+45-20| ... | @@ -310,6 +310,11 @@ pub const Connection = enum { | ... | @@ -310,6 +310,11 @@ pub const Connection = enum { |
| 310 | close, | 310 | close, |
| 311 | }; | 311 | }; |
| 312 | 312 | ||
| 313 | pub const Side = enum { | ||
| 314 | request, | ||
| 315 | response, | ||
| 316 | }; | ||
| 317 | |||
| 313 | pub const Header = struct { | 318 | pub const Header = struct { |
| 314 | name: []const u8, | 319 | name: []const u8, |
| 315 | value: []const u8, | 320 | value: []const u8, |
| ... | @@ -333,6 +338,8 @@ pub const Reader = struct { | ... | @@ -333,6 +338,8 @@ pub const Reader = struct { |
| 333 | trailers: []const u8 = &.{}, | 338 | trailers: []const u8 = &.{}, |
| 334 | body_err: ?BodyError = null, | 339 | body_err: ?BodyError = null, |
| 335 | max_head_len: usize, | 340 | max_head_len: usize, |
| 341 | /// Whether this is a reader over a request, sent by a client; or a response, sent by a server. | ||
| 342 | side: Side, | ||
| 336 | 343 | ||
| 337 | pub const RemainingChunkLen = enum(u64) { | 344 | pub const RemainingChunkLen = enum(u64) { |
| 338 | head = 0, | 345 | head = 0, |
| ... | @@ -458,8 +465,17 @@ pub const Reader = struct { | ... | @@ -458,8 +465,17 @@ pub const Reader = struct { |
| 458 | }; | 465 | }; |
| 459 | return &reader.interface; | 466 | return &reader.interface; |
| 460 | } else { | 467 | } else { |
| 461 | reader.state = .body_none; | 468 | return switch (reader.side) { |
| 462 | return reader.in; | 469 | .response => resp: { |
| 470 | reader.state = .body_none; | ||
| 471 | break :resp reader.in; | ||
| 472 | }, | ||
| 473 | .request => req: { | ||
| 474 | // RFC7230: If a request has no Content-Length and no Transfer-Encoding, it has no message body. | ||
| 475 | reader.state = .ready; | ||
| 476 | break :req .ending; | ||
| 477 | }, | ||
| 478 | }; | ||
| 463 | } | 479 | } |
| 464 | }, | 480 | }, |
| 465 | } | 481 | } |
| ... | @@ -746,6 +762,9 @@ pub const BodyWriter = struct { | ... | @@ -746,6 +762,9 @@ pub const BodyWriter = struct { |
| 746 | http_protocol_output: *Writer, | 762 | http_protocol_output: *Writer, |
| 747 | state: State, | 763 | state: State, |
| 748 | writer: Writer, | 764 | writer: Writer, |
| 765 | /// Associated `Client.Request`, if applicable, used to tell the request | ||
| 766 | /// that the body has been written and the connection is safe to reuse. | ||
| 767 | client_request: ?*Client.Request = null, | ||
| 749 | 768 | ||
| 750 | pub const Error = Writer.Error; | 769 | pub const Error = Writer.Error; |
| 751 | 770 | ||
| ... | @@ -824,8 +843,13 @@ pub const BodyWriter = struct { | ... | @@ -824,8 +843,13 @@ pub const BodyWriter = struct { |
| 824 | assert(len == 0); // Trips when end() called before all bytes written. | 843 | assert(len == 0); // Trips when end() called before all bytes written. |
| 825 | w.state = .end; | 844 | w.state = .end; |
| 826 | }, | 845 | }, |
| 827 | .none => {}, | ||
| 828 | .chunk_len => return endChunkedUnflushed(w, .{}), | 846 | .chunk_len => return endChunkedUnflushed(w, .{}), |
| 847 | // Don't clear payload_pending to prevent an unframed request from | ||
| 848 | // returning to the pool | ||
| 849 | .none => return, | ||
| 850 | } | ||
| 851 | if (w.client_request) |request| { | ||
| 852 | request.payload_pending = false; | ||
| 829 | } | 853 | } |
| 830 | } | 854 | } |
| 831 | 855 | ||
| ... | @@ -861,26 +885,27 @@ pub const BodyWriter = struct { | ... | @@ -861,26 +885,27 @@ pub const BodyWriter = struct { |
| 861 | /// * `endUnflushed` | 885 | /// * `endUnflushed` |
| 862 | /// * `end` | 886 | /// * `end` |
| 863 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { | 887 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { |
| 864 | if (w.isEliding()) { | 888 | if (!w.isEliding()) { |
| 865 | w.state = .end; | 889 | const bw = w.http_protocol_output; |
| 866 | return; | 890 | switch (w.state.chunk_len) { |
| 867 | } | 891 | 0 => {}, |
| 868 | const bw = w.http_protocol_output; | 892 | 1 => unreachable, // Wrote more data than specified in chunk header. |
| 869 | switch (w.state.chunk_len) { | 893 | 2 => try bw.writeAll("\r\n"), |
| 870 | 0 => {}, | 894 | else => unreachable, // An earlier write call indicated more data would follow. |
| 871 | 1 => unreachable, // Wrote more data than specified in chunk header. | 895 | } |
| 872 | 2 => try bw.writeAll("\r\n"), | 896 | try bw.writeAll("0\r\n"); |
| 873 | else => unreachable, // An earlier write call indicated more data would follow. | 897 | for (options.trailers) |trailer| { |
| 874 | } | 898 | try bw.writeAll(trailer.name); |
| 875 | try bw.writeAll("0\r\n"); | 899 | try bw.writeAll(": "); |
| 876 | for (options.trailers) |trailer| { | 900 | try bw.writeAll(trailer.value); |
| 877 | try bw.writeAll(trailer.name); | 901 | try bw.writeAll("\r\n"); |
| 878 | try bw.writeAll(": "); | 902 | } |
| 879 | try bw.writeAll(trailer.value); | ||
| 880 | try bw.writeAll("\r\n"); | 903 | try bw.writeAll("\r\n"); |
| 881 | } | 904 | } |
| 882 | try bw.writeAll("\r\n"); | ||
| 883 | w.state = .end; | 905 | w.state = .end; |
| 906 | if (w.client_request) |request| { | ||
| 907 | request.payload_pending = false; | ||
| 908 | } | ||
| 884 | } | 909 | } |
| 885 | 910 | ||
| 886 | pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { | 911 | pub fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { |
lib/std/http/Client.zig+15-7| ... | @@ -731,7 +731,7 @@ pub const Response = struct { | ... | @@ -731,7 +731,7 @@ pub const Response = struct { |
| 731 | pub fn reader(response: *Response, transfer_buffer: []u8) *Reader { | 731 | pub fn reader(response: *Response, transfer_buffer: []u8) *Reader { |
| 732 | response.head.invalidateStrings(); | 732 | response.head.invalidateStrings(); |
| 733 | const req = response.request; | 733 | const req = response.request; |
| 734 | if (!req.method.responseHasBody()) return .ending; | 734 | if (req.method == .HEAD) return .ending; |
| 735 | const head = &response.head; | 735 | const head = &response.head; |
| 736 | return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length); | 736 | return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length); |
| 737 | } | 737 | } |
| ... | @@ -807,11 +807,14 @@ pub const Request = struct { | ... | @@ -807,11 +807,14 @@ pub const Request = struct { |
| 807 | headers: Headers, | 807 | headers: Headers, |
| 808 | 808 | ||
| 809 | /// Populated in `receiveHead`; used in `deinit` to determine whether to | 809 | /// Populated in `receiveHead`; used in `deinit` to determine whether to |
| 810 | /// discard the body to reuse the connection. | 810 | /// discard the response body to reuse the connection. |
| 811 | response_content_length: ?u64 = null, | 811 | response_content_length: ?u64 = null, |
| 812 | /// Populated in `receiveHead`; used in `deinit` to determine whether to | 812 | /// Populated in `receiveHead`; used in `deinit` to determine whether to |
| 813 | /// discard the body to reuse the connection. | 813 | /// discard the response body to reuse the connection. |
| 814 | response_transfer_encoding: http.TransferEncoding = .none, | 814 | response_transfer_encoding: http.TransferEncoding = .none, |
| 815 | /// Used in `deinit` to determine whether the payload has been sent, making | ||
| 816 | /// it safe to reuse the connection. | ||
| 817 | payload_pending: bool = false, | ||
| 815 | 818 | ||
| 816 | /// These headers are kept including when following a redirect to a | 819 | /// These headers are kept including when following a redirect to a |
| 817 | /// different domain. | 820 | /// different domain. |
| ... | @@ -888,8 +891,9 @@ pub const Request = struct { | ... | @@ -888,8 +891,9 @@ pub const Request = struct { |
| 888 | connection.closing = connection.closing or switch (r.reader.state) { | 891 | connection.closing = connection.closing or switch (r.reader.state) { |
| 889 | .ready => false, | 892 | .ready => false, |
| 890 | .received_head => c: { | 893 | .received_head => c: { |
| 891 | if (r.method.requestHasBody()) break :c true; | 894 | if (r.payload_pending) break :c true; |
| 892 | if (!r.method.responseHasBody()) break :c false; | 895 | if (r.method == .HEAD) break :c false; |
| 896 | |||
| 893 | const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); | 897 | const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); |
| 894 | _ = reader.discardRemaining() catch |err| switch (err) { | 898 | _ = reader.discardRemaining() catch |err| switch (err) { |
| 895 | error.ReadFailed => break :c true, | 899 | error.ReadFailed => break :c true, |
| ... | @@ -912,7 +916,6 @@ pub const Request = struct { | ... | @@ -912,7 +916,6 @@ pub const Request = struct { |
| 912 | /// Sends but does not flush a complete request as only HTTP head, no body. | 916 | /// Sends but does not flush a complete request as only HTTP head, no body. |
| 913 | pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { | 917 | pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { |
| 914 | assert(r.transfer_encoding == .none); | 918 | assert(r.transfer_encoding == .none); |
| 915 | assert(!r.method.requestHasBody()); | ||
| 916 | try sendHead(r); | 919 | try sendHead(r); |
| 917 | } | 920 | } |
| 918 | 921 | ||
| ... | @@ -941,13 +944,14 @@ pub const Request = struct { | ... | @@ -941,13 +944,14 @@ pub const Request = struct { |
| 941 | /// See also: | 944 | /// See also: |
| 942 | /// * `sendBody` | 945 | /// * `sendBody` |
| 943 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | 946 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { |
| 944 | assert(r.method.requestHasBody()); | 947 | r.payload_pending = true; |
| 945 | try sendHead(r); | 948 | try sendHead(r); |
| 946 | const http_protocol_output = r.connection.?.writer(); | 949 | const http_protocol_output = r.connection.?.writer(); |
| 947 | return switch (r.transfer_encoding) { | 950 | return switch (r.transfer_encoding) { |
| 948 | .chunked => .{ | 951 | .chunked => .{ |
| 949 | .http_protocol_output = http_protocol_output, | 952 | .http_protocol_output = http_protocol_output, |
| 950 | .state = .init_chunked, | 953 | .state = .init_chunked, |
| 954 | .client_request = r, | ||
| 951 | .writer = .{ | 955 | .writer = .{ |
| 952 | .buffer = buffer, | 956 | .buffer = buffer, |
| 953 | .vtable = &.{ | 957 | .vtable = &.{ |
| ... | @@ -959,6 +963,7 @@ pub const Request = struct { | ... | @@ -959,6 +963,7 @@ pub const Request = struct { |
| 959 | .content_length => |len| .{ | 963 | .content_length => |len| .{ |
| 960 | .http_protocol_output = http_protocol_output, | 964 | .http_protocol_output = http_protocol_output, |
| 961 | .state = .{ .content_length = len }, | 965 | .state = .{ .content_length = len }, |
| 966 | .client_request = r, | ||
| 962 | .writer = .{ | 967 | .writer = .{ |
| 963 | .buffer = buffer, | 968 | .buffer = buffer, |
| 964 | .vtable = &.{ | 969 | .vtable = &.{ |
| ... | @@ -970,6 +975,7 @@ pub const Request = struct { | ... | @@ -970,6 +975,7 @@ pub const Request = struct { |
| 970 | .none => .{ | 975 | .none => .{ |
| 971 | .http_protocol_output = http_protocol_output, | 976 | .http_protocol_output = http_protocol_output, |
| 972 | .state = .none, | 977 | .state = .none, |
| 978 | .client_request = r, | ||
| 973 | .writer = .{ | 979 | .writer = .{ |
| 974 | .buffer = buffer, | 980 | .buffer = buffer, |
| 975 | .vtable = &.{ | 981 | .vtable = &.{ |
| ... | @@ -1273,6 +1279,7 @@ pub const Request = struct { | ... | @@ -1273,6 +1279,7 @@ pub const Request = struct { |
| 1273 | // Populated when `http.Reader.bodyReader` is called. | 1279 | // Populated when `http.Reader.bodyReader` is called. |
| 1274 | .interface = undefined, | 1280 | .interface = undefined, |
| 1275 | .max_head_len = r.client.read_buffer_size, | 1281 | .max_head_len = r.client.read_buffer_size, |
| 1282 | .side = .response, | ||
| 1276 | }; | 1283 | }; |
| 1277 | r.redirect_behavior.subtractOne(); | 1284 | r.redirect_behavior.subtractOne(); |
| 1278 | } | 1285 | } |
| ... | @@ -1744,6 +1751,7 @@ pub fn request( | ... | @@ -1744,6 +1751,7 @@ pub fn request( |
| 1744 | // Populated when `http.Reader.bodyReader` is called. | 1751 | // Populated when `http.Reader.bodyReader` is called. |
| 1745 | .interface = undefined, | 1752 | .interface = undefined, |
| 1746 | .max_head_len = client.read_buffer_size, | 1753 | .max_head_len = client.read_buffer_size, |
| 1754 | .side = .response, | ||
| 1747 | }, | 1755 | }, |
| 1748 | .keep_alive = options.keep_alive, | 1756 | .keep_alive = options.keep_alive, |
| 1749 | .method = method, | 1757 | .method = method, |
lib/std/http/Server.zig+4-3| ... | @@ -30,6 +30,7 @@ pub fn init(in: *Reader, out: *Writer) Server { | ... | @@ -30,6 +30,7 @@ pub fn init(in: *Reader, out: *Writer) Server { |
| 30 | // Populated when `http.Reader.bodyReader` is called. | 30 | // Populated when `http.Reader.bodyReader` is called. |
| 31 | .interface = undefined, | 31 | .interface = undefined, |
| 32 | .max_head_len = in.buffer.len, | 32 | .max_head_len = in.buffer.len, |
| 33 | .side = .request, | ||
| 33 | }, | 34 | }, |
| 34 | .out = out, | 35 | .out = out, |
| 35 | }; | 36 | }; |
| ... | @@ -253,6 +254,7 @@ pub const Request = struct { | ... | @@ -253,6 +254,7 @@ pub const Request = struct { |
| 253 | .state = .received_head, | 254 | .state = .received_head, |
| 254 | .interface = undefined, | 255 | .interface = undefined, |
| 255 | .max_head_len = 4096, | 256 | .max_head_len = 4096, |
| 257 | .side = .request, | ||
| 256 | }, | 258 | }, |
| 257 | .out = undefined, | 259 | .out = undefined, |
| 258 | }; | 260 | }; |
| ... | @@ -592,7 +594,7 @@ pub const Request = struct { | ... | @@ -592,7 +594,7 @@ pub const Request = struct { |
| 592 | assert(request.server.reader.state == .received_head); | 594 | assert(request.server.reader.state == .received_head); |
| 593 | assert(request.head.expect == null); | 595 | assert(request.head.expect == null); |
| 594 | request.head.invalidateStrings(); | 596 | request.head.invalidateStrings(); |
| 595 | if (!request.head.method.requestHasBody()) return .ending; | 597 | if (request.head.method == .HEAD) return .ending; |
| 596 | return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length); | 598 | return request.server.reader.bodyReader(buffer, request.head.transfer_encoding, request.head.content_length); |
| 597 | } | 599 | } |
| 598 | 600 | ||
| ... | @@ -627,8 +629,7 @@ pub const Request = struct { | ... | @@ -627,8 +629,7 @@ pub const Request = struct { |
| 627 | const r = &request.server.reader; | 629 | const r = &request.server.reader; |
| 628 | if (keep_alive and request.head.keep_alive) switch (r.state) { | 630 | if (keep_alive and request.head.keep_alive) switch (r.state) { |
| 629 | .received_head => { | 631 | .received_head => { |
| 630 | if (request.head.method.requestHasBody()) { | 632 | if (request.head.transfer_encoding != .none or request.head.content_length != null) { |
| 631 | assert(request.head.transfer_encoding != .none or request.head.content_length != null); | ||
| 632 | const reader_interface = request.readerExpectContinue(&.{}) catch return false; | 633 | const reader_interface = request.readerExpectContinue(&.{}) catch return false; |
| 633 | _ = reader_interface.discardRemaining() catch return false; | 634 | _ = reader_interface.discardRemaining() catch return false; |
| 634 | assert(r.state == .ready); | 635 | assert(r.state == .ready); |
lib/std/http/test.zig+137| ... | @@ -17,6 +17,7 @@ test "content length reader state update" { | ... | @@ -17,6 +17,7 @@ test "content length reader state update" { |
| 17 | .interface = undefined, | 17 | .interface = undefined, |
| 18 | .state = .ready, | 18 | .state = .ready, |
| 19 | .max_head_len = 1024, | 19 | .max_head_len = 1024, |
| 20 | .side = .request, | ||
| 20 | }; | 21 | }; |
| 21 | 22 | ||
| 22 | _ = try reader.receiveHead(); | 23 | _ = try reader.receiveHead(); |
| ... | @@ -1123,6 +1124,27 @@ fn echoTests(client: *http.Client, port: u16) !void { | ... | @@ -1123,6 +1124,27 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1123 | try expectEqual(.expectation_failed, response.head.status); | 1124 | try expectEqual(.expectation_failed, response.head.status); |
| 1124 | _ = try response.reader(&.{}).discardRemaining(); | 1125 | _ = try response.reader(&.{}).discardRemaining(); |
| 1125 | } | 1126 | } |
| 1127 | |||
| 1128 | { // Send GET with body | ||
| 1129 | const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo-content#get-with-body", .{port}); | ||
| 1130 | defer gpa.free(location); | ||
| 1131 | |||
| 1132 | var body: std.Io.Writer.Allocating = .init(gpa); | ||
| 1133 | defer body.deinit(); | ||
| 1134 | try body.ensureUnusedCapacity(64); | ||
| 1135 | |||
| 1136 | const res = try client.fetch(.{ | ||
| 1137 | .location = .{ .url = location }, | ||
| 1138 | .method = .GET, | ||
| 1139 | .payload = "Hello, World!\n", | ||
| 1140 | .extra_headers = &.{ | ||
| 1141 | .{ .name = "content-type", .value = "text/plain" }, | ||
| 1142 | }, | ||
| 1143 | .response_writer = &body.writer, | ||
| 1144 | }); | ||
| 1145 | try expectEqual(.ok, res.status); | ||
| 1146 | try expectEqualStrings("Hello, World!\n", body.written()); | ||
| 1147 | } | ||
| 1126 | } | 1148 | } |
| 1127 | 1149 | ||
| 1128 | const TestServer = struct { | 1150 | const TestServer = struct { |
| ... | @@ -1320,3 +1342,118 @@ test "boot failed connections from the pool" { | ... | @@ -1320,3 +1342,118 @@ test "boot failed connections from the pool" { |
| 1320 | try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } })); | 1342 | try expectError(error.HttpConnectionClosing, client.fetch(.{ .location = .{ .uri = uri } })); |
| 1321 | } | 1343 | } |
| 1322 | } | 1344 | } |
| 1345 | |||
| 1346 | test "send and discard or echo empty bodies" { | ||
| 1347 | if (builtin.cpu.arch.isPowerPC64() and builtin.mode != .debug) return error.SkipZigTest; // https://github.com/llvm/llvm-project/issues/194257 | ||
| 1348 | if (builtin.os.tag == .openbsd) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30806 | ||
| 1349 | |||
| 1350 | const io = std.testing.io; | ||
| 1351 | const test_server = try createTestServer(io, struct { | ||
| 1352 | fn run(test_server: *TestServer) anyerror!void { | ||
| 1353 | const net_server = &test_server.net_server; | ||
| 1354 | var recv_buffer: [0x100]u8 = undefined; | ||
| 1355 | var send_buffer: [0x100]u8 = undefined; | ||
| 1356 | |||
| 1357 | accept: while (!test_server.shutting_down) { | ||
| 1358 | var stream = try net_server.accept(io); | ||
| 1359 | defer stream.close(io); | ||
| 1360 | |||
| 1361 | var connection_br = stream.reader(io, &recv_buffer); | ||
| 1362 | var connection_bw = stream.writer(io, &send_buffer); | ||
| 1363 | var http_server = http.Server.init(&connection_br.interface, &connection_bw.interface); | ||
| 1364 | |||
| 1365 | while (http_server.reader.state == .ready) { | ||
| 1366 | var request = http_server.receiveHead() catch |err| switch (err) { | ||
| 1367 | error.HttpConnectionClosing => continue :accept, | ||
| 1368 | else => |e| return e, | ||
| 1369 | }; | ||
| 1370 | if (mem.eql(u8, request.head.target, "/discard")) { | ||
| 1371 | try request.respond("ignored", .{}); | ||
| 1372 | } else if (mem.eql(u8, request.head.target, "/echo")) { | ||
| 1373 | const body = try (try request.readerExpectContinue(&.{})).allocRemaining(std.testing.allocator, .unlimited); | ||
| 1374 | try request.respond(body, .{}); | ||
| 1375 | } | ||
| 1376 | } | ||
| 1377 | } | ||
| 1378 | } | ||
| 1379 | }); | ||
| 1380 | defer test_server.destroy(); | ||
| 1381 | |||
| 1382 | const gpa = std.testing.allocator; | ||
| 1383 | var client: http.Client = .{ .allocator = gpa, .io = io }; | ||
| 1384 | defer client.deinit(); | ||
| 1385 | |||
| 1386 | const port = test_server.port(); | ||
| 1387 | |||
| 1388 | const location_discard = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/discard", .{port}); | ||
| 1389 | defer gpa.free(location_discard); | ||
| 1390 | const uri_discard = try std.Uri.parse(location_discard); | ||
| 1391 | |||
| 1392 | const location_echo = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/echo", .{port}); | ||
| 1393 | defer gpa.free(location_echo); | ||
| 1394 | const uri_echo = try std.Uri.parse(location_echo); | ||
| 1395 | |||
| 1396 | { // Echo POST with sendBodiless | ||
| 1397 | var response_buffer: [0x100]u8 = undefined; | ||
| 1398 | |||
| 1399 | var request = try client.request(.POST, uri_echo, .{}); | ||
| 1400 | defer request.deinit(); | ||
| 1401 | try request.sendBodiless(); | ||
| 1402 | var response = try request.receiveHead(&response_buffer); | ||
| 1403 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | ||
| 1404 | defer gpa.free(body); | ||
| 1405 | |||
| 1406 | try expectEqualStrings("", body); | ||
| 1407 | } | ||
| 1408 | |||
| 1409 | // connection has been kept alive | ||
| 1410 | try expect(client.http_proxy != null or client.connection_pool.free_len == 1); | ||
| 1411 | |||
| 1412 | { // Discard POST with sendBodiless | ||
| 1413 | var response_buffer: [0x100]u8 = undefined; | ||
| 1414 | |||
| 1415 | var request = try client.request(.POST, uri_discard, .{}); | ||
| 1416 | defer request.deinit(); | ||
| 1417 | try request.sendBodiless(); | ||
| 1418 | var response = try request.receiveHead(&response_buffer); | ||
| 1419 | const body = try response.reader(&.{}).allocRemaining(gpa, .unlimited); | ||
| 1420 | defer gpa.free(body); | ||
| 1421 | |||
| 1422 | try expectEqualStrings("ignored", body); | ||
| 1423 | } | ||
| 1424 | |||
| 1425 | // connection has been kept alive | ||
| 1426 | try expect(client.http_proxy != null or client.connection_pool.free_len == 1); | ||
| 1427 | |||
| 1428 | { // Discard POST with empty fetch payload | ||
| 1429 | var body: std.Io.Writer.Allocating = .init(gpa); | ||
| 1430 | defer body.deinit(); | ||
| 1431 | |||
| 1432 | _ = try client.fetch(.{ | ||
| 1433 | .method = .POST, | ||
| 1434 | .location = .{ .uri = uri_discard }, | ||
| 1435 | .response_writer = &body.writer, | ||
| 1436 | .payload = "", | ||
| 1437 | }); | ||
| 1438 | try expectEqualStrings("ignored", body.written()); | ||
| 1439 | } | ||
| 1440 | |||
| 1441 | // connection has been kept alive | ||
| 1442 | try expect(client.http_proxy != null or client.connection_pool.free_len == 1); | ||
| 1443 | |||
| 1444 | { // Echo POST with empty fetch payload | ||
| 1445 | var body: std.Io.Writer.Allocating = .init(gpa); | ||
| 1446 | defer body.deinit(); | ||
| 1447 | |||
| 1448 | _ = try client.fetch(.{ | ||
| 1449 | .method = .POST, | ||
| 1450 | .location = .{ .uri = uri_echo }, | ||
| 1451 | .response_writer = &body.writer, | ||
| 1452 | .payload = "", | ||
| 1453 | }); | ||
| 1454 | try expectEqualStrings("", body.written()); | ||
| 1455 | } | ||
| 1456 | |||
| 1457 | // connection has been kept alive | ||
| 1458 | try expect(client.http_proxy != null or client.connection_pool.free_len == 1); | ||
| 1459 | } |