| author | |
| committer | |
| log | 15c1d3132b1edc61e33310e195b16e9a02bcacdd |
| tree | 169f0e98975c366147dba0b3a1bf2c660527dec4 |
| parent | eb4b3eb34f729b89be4085dbca32c00532f48666 |
| signature | Signed by SSH key SHA256:7B/LJ7bpR1eX8aCXSr4mtd5M45VMPKcx9zY8e95b5QM |
4 files changed, 201 insertions(+), 30 deletions(-)
lib/std/http.zig+45-20| ... | ... | @@ -310,6 +310,11 @@ pub const Connection = enum { |
| 310 | 310 | close, |
| 311 | 311 | }; |
| 312 | 312 | |
| 313 | pub const Side = enum { | |
| 314 | request, | |
| 315 | response, | |
| 316 | }; | |
| 317 | ||
| 313 | 318 | pub const Header = struct { |
| 314 | 319 | name: []const u8, |
| 315 | 320 | value: []const u8, |
| ... | ... | @@ -333,6 +338,8 @@ pub const Reader = struct { |
| 333 | 338 | trailers: []const u8 = &.{}, |
| 334 | 339 | body_err: ?BodyError = null, |
| 335 | 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 | 344 | pub const RemainingChunkLen = enum(u64) { |
| 338 | 345 | head = 0, |
| ... | ... | @@ -458,8 +465,17 @@ pub const Reader = struct { |
| 458 | 465 | }; |
| 459 | 466 | return &reader.interface; |
| 460 | 467 | } else { |
| 461 | reader.state = .body_none; | |
| 462 | return reader.in; | |
| 468 | return switch (reader.side) { | |
| 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 | 762 | http_protocol_output: *Writer, |
| 747 | 763 | state: State, |
| 748 | 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 | 769 | pub const Error = Writer.Error; |
| 751 | 770 | |
| ... | ... | @@ -824,8 +843,13 @@ pub const BodyWriter = struct { |
| 824 | 843 | assert(len == 0); // Trips when end() called before all bytes written. |
| 825 | 844 | w.state = .end; |
| 826 | 845 | }, |
| 827 | .none => {}, | |
| 828 | 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 | 885 | /// * `endUnflushed` |
| 862 | 886 | /// * `end` |
| 863 | 887 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { |
| 864 | if (w.isEliding()) { | |
| 865 | w.state = .end; | |
| 866 | return; | |
| 867 | } | |
| 868 | const bw = w.http_protocol_output; | |
| 869 | switch (w.state.chunk_len) { | |
| 870 | 0 => {}, | |
| 871 | 1 => unreachable, // Wrote more data than specified in chunk header. | |
| 872 | 2 => try bw.writeAll("\r\n"), | |
| 873 | else => unreachable, // An earlier write call indicated more data would follow. | |
| 874 | } | |
| 875 | try bw.writeAll("0\r\n"); | |
| 876 | for (options.trailers) |trailer| { | |
| 877 | try bw.writeAll(trailer.name); | |
| 878 | try bw.writeAll(": "); | |
| 879 | try bw.writeAll(trailer.value); | |
| 888 | if (!w.isEliding()) { | |
| 889 | const bw = w.http_protocol_output; | |
| 890 | switch (w.state.chunk_len) { | |
| 891 | 0 => {}, | |
| 892 | 1 => unreachable, // Wrote more data than specified in chunk header. | |
| 893 | 2 => try bw.writeAll("\r\n"), | |
| 894 | else => unreachable, // An earlier write call indicated more data would follow. | |
| 895 | } | |
| 896 | try bw.writeAll("0\r\n"); | |
| 897 | for (options.trailers) |trailer| { | |
| 898 | try bw.writeAll(trailer.name); | |
| 899 | try bw.writeAll(": "); | |
| 900 | try bw.writeAll(trailer.value); | |
| 901 | try bw.writeAll("\r\n"); | |
| 902 | } | |
| 880 | 903 | try bw.writeAll("\r\n"); |
| 881 | 904 | } |
| 882 | try bw.writeAll("\r\n"); | |
| 883 | 905 | w.state = .end; |
| 906 | if (w.client_request) |request| { | |
| 907 | request.payload_pending = false; | |
| 908 | } | |
| 884 | 909 | } |
| 885 | 910 | |
| 886 | 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 | 731 | pub fn reader(response: *Response, transfer_buffer: []u8) *Reader { |
| 732 | 732 | response.head.invalidateStrings(); |
| 733 | 733 | const req = response.request; |
| 734 | if (!req.method.responseHasBody()) return .ending; | |
| 734 | if (req.method == .HEAD) return .ending; | |
| 735 | 735 | const head = &response.head; |
| 736 | 736 | return req.reader.bodyReader(transfer_buffer, head.transfer_encoding, head.content_length); |
| 737 | 737 | } |
| ... | ... | @@ -807,11 +807,14 @@ pub const Request = struct { |
| 807 | 807 | headers: Headers, |
| 808 | 808 | |
| 809 | 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 | 811 | response_content_length: ?u64 = null, |
| 812 | 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 | 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 | 819 | /// These headers are kept including when following a redirect to a |
| 817 | 820 | /// different domain. |
| ... | ... | @@ -888,8 +891,9 @@ pub const Request = struct { |
| 888 | 891 | connection.closing = connection.closing or switch (r.reader.state) { |
| 889 | 892 | .ready => false, |
| 890 | 893 | .received_head => c: { |
| 891 | if (r.method.requestHasBody()) break :c true; | |
| 892 | if (!r.method.responseHasBody()) break :c false; | |
| 894 | if (r.payload_pending) break :c true; | |
| 895 | if (r.method == .HEAD) break :c false; | |
| 896 | ||
| 893 | 897 | const reader = r.reader.bodyReader(&.{}, r.response_transfer_encoding, r.response_content_length); |
| 894 | 898 | _ = reader.discardRemaining() catch |err| switch (err) { |
| 895 | 899 | error.ReadFailed => break :c true, |
| ... | ... | @@ -912,7 +916,6 @@ pub const Request = struct { |
| 912 | 916 | /// Sends but does not flush a complete request as only HTTP head, no body. |
| 913 | 917 | pub fn sendBodilessUnflushed(r: *Request) Writer.Error!void { |
| 914 | 918 | assert(r.transfer_encoding == .none); |
| 915 | assert(!r.method.requestHasBody()); | |
| 916 | 919 | try sendHead(r); |
| 917 | 920 | } |
| 918 | 921 | |
| ... | ... | @@ -941,13 +944,14 @@ pub const Request = struct { |
| 941 | 944 | /// See also: |
| 942 | 945 | /// * `sendBody` |
| 943 | 946 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { |
| 944 | assert(r.method.requestHasBody()); | |
| 947 | r.payload_pending = true; | |
| 945 | 948 | try sendHead(r); |
| 946 | 949 | const http_protocol_output = r.connection.?.writer(); |
| 947 | 950 | return switch (r.transfer_encoding) { |
| 948 | 951 | .chunked => .{ |
| 949 | 952 | .http_protocol_output = http_protocol_output, |
| 950 | 953 | .state = .init_chunked, |
| 954 | .client_request = r, | |
| 951 | 955 | .writer = .{ |
| 952 | 956 | .buffer = buffer, |
| 953 | 957 | .vtable = &.{ |
| ... | ... | @@ -959,6 +963,7 @@ pub const Request = struct { |
| 959 | 963 | .content_length => |len| .{ |
| 960 | 964 | .http_protocol_output = http_protocol_output, |
| 961 | 965 | .state = .{ .content_length = len }, |
| 966 | .client_request = r, | |
| 962 | 967 | .writer = .{ |
| 963 | 968 | .buffer = buffer, |
| 964 | 969 | .vtable = &.{ |
| ... | ... | @@ -970,6 +975,7 @@ pub const Request = struct { |
| 970 | 975 | .none => .{ |
| 971 | 976 | .http_protocol_output = http_protocol_output, |
| 972 | 977 | .state = .none, |
| 978 | .client_request = r, | |
| 973 | 979 | .writer = .{ |
| 974 | 980 | .buffer = buffer, |
| 975 | 981 | .vtable = &.{ |
| ... | ... | @@ -1273,6 +1279,7 @@ pub const Request = struct { |
| 1273 | 1279 | // Populated when `http.Reader.bodyReader` is called. |
| 1274 | 1280 | .interface = undefined, |
| 1275 | 1281 | .max_head_len = r.client.read_buffer_size, |
| 1282 | .side = .response, | |
| 1276 | 1283 | }; |
| 1277 | 1284 | r.redirect_behavior.subtractOne(); |
| 1278 | 1285 | } |
| ... | ... | @@ -1744,6 +1751,7 @@ pub fn request( |
| 1744 | 1751 | // Populated when `http.Reader.bodyReader` is called. |
| 1745 | 1752 | .interface = undefined, |
| 1746 | 1753 | .max_head_len = client.read_buffer_size, |
| 1754 | .side = .response, | |
| 1747 | 1755 | }, |
| 1748 | 1756 | .keep_alive = options.keep_alive, |
| 1749 | 1757 | .method = method, |
lib/std/http/Server.zig+4-3| ... | ... | @@ -30,6 +30,7 @@ pub fn init(in: *Reader, out: *Writer) Server { |
| 30 | 30 | // Populated when `http.Reader.bodyReader` is called. |
| 31 | 31 | .interface = undefined, |
| 32 | 32 | .max_head_len = in.buffer.len, |
| 33 | .side = .request, | |
| 33 | 34 | }, |
| 34 | 35 | .out = out, |
| 35 | 36 | }; |
| ... | ... | @@ -253,6 +254,7 @@ pub const Request = struct { |
| 253 | 254 | .state = .received_head, |
| 254 | 255 | .interface = undefined, |
| 255 | 256 | .max_head_len = 4096, |
| 257 | .side = .request, | |
| 256 | 258 | }, |
| 257 | 259 | .out = undefined, |
| 258 | 260 | }; |
| ... | ... | @@ -592,7 +594,7 @@ pub const Request = struct { |
| 592 | 594 | assert(request.server.reader.state == .received_head); |
| 593 | 595 | assert(request.head.expect == null); |
| 594 | 596 | request.head.invalidateStrings(); |
| 595 | if (!request.head.method.requestHasBody()) return .ending; | |
| 597 | if (request.head.method == .HEAD) return .ending; | |
| 596 | 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 | 629 | const r = &request.server.reader; |
| 628 | 630 | if (keep_alive and request.head.keep_alive) switch (r.state) { |
| 629 | 631 | .received_head => { |
| 630 | if (request.head.method.requestHasBody()) { | |
| 631 | assert(request.head.transfer_encoding != .none or request.head.content_length != null); | |
| 632 | if (request.head.transfer_encoding != .none or request.head.content_length != null) { | |
| 632 | 633 | const reader_interface = request.readerExpectContinue(&.{}) catch return false; |
| 633 | 634 | _ = reader_interface.discardRemaining() catch return false; |
| 634 | 635 | assert(r.state == .ready); |
lib/std/http/test.zig+137| ... | ... | @@ -17,6 +17,7 @@ test "content length reader state update" { |
| 17 | 17 | .interface = undefined, |
| 18 | 18 | .state = .ready, |
| 19 | 19 | .max_head_len = 1024, |
| 20 | .side = .request, | |
| 20 | 21 | }; |
| 21 | 22 | |
| 22 | 23 | _ = try reader.receiveHead(); |
| ... | ... | @@ -1123,6 +1124,27 @@ fn echoTests(client: *http.Client, port: u16) !void { |
| 1123 | 1124 | try expectEqual(.expectation_failed, response.head.status); |
| 1124 | 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 | 1150 | const TestServer = struct { |
| ... | ... | @@ -1320,3 +1342,118 @@ test "boot failed connections from the pool" { |
| 1320 | 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 | } |