| author | |
| committer | |
| log | aa090a49d94155c4804644377db110f3b13f0500 |
| tree | 06226127b3e279414c6a8f1ca2a12f7c6b75f308 |
| parent | 5d40338f21b468c82d4bc2a1ac0a35c643126e74 |
| signature |
4 files changed, 156 insertions(+), 42 deletions(-)
lib/std/http/Client.zig+40-5| ... | @@ -478,6 +478,7 @@ pub const Request = struct { | ... | @@ -478,6 +478,7 @@ pub const Request = struct { |
| 478 | .zstd => |*zstd| zstd.deinit(), | 478 | .zstd => |*zstd| zstd.deinit(), |
| 479 | } | 479 | } |
| 480 | 480 | ||
| 481 | req.headers.deinit(); | ||
| 481 | req.response.headers.deinit(); | 482 | req.response.headers.deinit(); |
| 482 | 483 | ||
| 483 | if (req.response.parser.header_bytes_owned) { | 484 | if (req.response.parser.header_bytes_owned) { |
| ... | @@ -667,17 +668,19 @@ pub const Request = struct { | ... | @@ -667,17 +668,19 @@ pub const Request = struct { |
| 667 | 668 | ||
| 668 | try req.response.parse(req.response.parser.header_bytes.items, false); | 669 | try req.response.parse(req.response.parser.header_bytes.items, false); |
| 669 | 670 | ||
| 670 | if (req.response.status == .switching_protocols) { | 671 | if (req.response.status == .@"continue") { |
| 671 | req.connection.?.data.closing = false; | 672 | req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response |
| 672 | req.response.parser.done = true; | 673 | req.response.parser.reset(); |
| 674 | break; | ||
| 673 | } | 675 | } |
| 674 | 676 | ||
| 675 | if (req.method == .CONNECT and req.response.status == .ok) { | 677 | // we're switching protocols, so this connection is no longer doing http |
| 678 | if (req.response.status == .switching_protocols or (req.method == .CONNECT and req.response.status == .ok)) { | ||
| 676 | req.connection.?.data.closing = false; | 679 | req.connection.?.data.closing = false; |
| 677 | req.response.parser.done = true; | 680 | req.response.parser.done = true; |
| 678 | } | 681 | } |
| 679 | 682 | ||
| 680 | // we default to using keep-alive if not provided | 683 | // we default to using keep-alive if not provided in the client if the server asks for it |
| 681 | const req_connection = req.headers.getFirstValue("connection"); | 684 | const req_connection = req.headers.getFirstValue("connection"); |
| 682 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | 685 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); |
| 683 | 686 | ||
| ... | @@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: | ... | @@ -955,6 +958,38 @@ pub fn connectUnproxied(client: *Client, host: []const u8, port: u16, protocol: |
| 955 | return conn; | 958 | return conn; |
| 956 | } | 959 | } |
| 957 | 960 | ||
| 961 | pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{NameTooLong} || std.os.ConnectError; | ||
| 962 | |||
| 963 | pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*ConnectionPool.Node { | ||
| 964 | if (client.connection_pool.findConnection(.{ | ||
| 965 | .host = path, | ||
| 966 | .port = 0, | ||
| 967 | .is_tls = false, | ||
| 968 | })) |node| | ||
| 969 | return node; | ||
| 970 | |||
| 971 | const conn = try client.allocator.create(ConnectionPool.Node); | ||
| 972 | errdefer client.allocator.destroy(conn); | ||
| 973 | conn.* = .{ .data = undefined }; | ||
| 974 | |||
| 975 | const stream = try std.net.connectUnixSocket(path); | ||
| 976 | errdefer stream.close(); | ||
| 977 | |||
| 978 | conn.data = .{ | ||
| 979 | .stream = stream, | ||
| 980 | .tls_client = undefined, | ||
| 981 | .protocol = .plain, | ||
| 982 | |||
| 983 | .host = try client.allocator.dupe(u8, path), | ||
| 984 | .port = 0, | ||
| 985 | }; | ||
| 986 | errdefer client.allocator.free(conn.data.host); | ||
| 987 | |||
| 988 | client.connection_pool.addUsed(conn); | ||
| 989 | |||
| 990 | return conn; | ||
| 991 | } | ||
| 992 | |||
| 958 | // Prevents a dependency loop in request() | 993 | // Prevents a dependency loop in request() |
| 959 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; | 994 | const ConnectErrorPartial = ConnectUnproxiedError || error{ UnsupportedUrlScheme, ConnectionRefused }; |
| 960 | pub const ConnectError = ConnectErrorPartial || RequestError; | 995 | pub const ConnectError = ConnectErrorPartial || RequestError; |
lib/std/http/Server.zig+41-33| ... | @@ -411,48 +411,52 @@ pub const Response = struct { | ... | @@ -411,48 +411,52 @@ pub const Response = struct { |
| 411 | } | 411 | } |
| 412 | try w.writeAll("\r\n"); | 412 | try w.writeAll("\r\n"); |
| 413 | 413 | ||
| 414 | if (!res.headers.contains("server")) { | 414 | if (res.status == .@"continue") { |
| 415 | try w.writeAll("Server: zig (std.http)\r\n"); | 415 | res.state = .waited; // we still need to send another request after this |
| 416 | } | 416 | } else { |
| 417 | if (!res.headers.contains("server")) { | ||
| 418 | try w.writeAll("Server: zig (std.http)\r\n"); | ||
| 419 | } | ||
| 417 | 420 | ||
| 418 | if (!res.headers.contains("connection")) { | 421 | if (!res.headers.contains("connection")) { |
| 419 | const req_connection = res.request.headers.getFirstValue("connection"); | 422 | const req_connection = res.request.headers.getFirstValue("connection"); |
| 420 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); | 423 | const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); |
| 421 | 424 | ||
| 422 | if (req_keepalive) { | 425 | if (req_keepalive) { |
| 423 | try w.writeAll("Connection: keep-alive\r\n"); | 426 | try w.writeAll("Connection: keep-alive\r\n"); |
| 424 | } else { | 427 | } else { |
| 425 | try w.writeAll("Connection: close\r\n"); | 428 | try w.writeAll("Connection: close\r\n"); |
| 429 | } | ||
| 426 | } | 430 | } |
| 427 | } | ||
| 428 | 431 | ||
| 429 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); | 432 | const has_transfer_encoding = res.headers.contains("transfer-encoding"); |
| 430 | const has_content_length = res.headers.contains("content-length"); | 433 | const has_content_length = res.headers.contains("content-length"); |
| 431 | 434 | ||
| 432 | if (!has_transfer_encoding and !has_content_length) { | 435 | if (!has_transfer_encoding and !has_content_length) { |
| 433 | switch (res.transfer_encoding) { | 436 | switch (res.transfer_encoding) { |
| 434 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), | 437 | .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), |
| 435 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), | 438 | .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), |
| 436 | .none => {}, | 439 | .none => {}, |
| 437 | } | ||
| 438 | } else { | ||
| 439 | if (has_content_length) { | ||
| 440 | const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; | ||
| 441 | |||
| 442 | res.transfer_encoding = .{ .content_length = content_length }; | ||
| 443 | } else if (has_transfer_encoding) { | ||
| 444 | const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?; | ||
| 445 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | ||
| 446 | res.transfer_encoding = .chunked; | ||
| 447 | } else { | ||
| 448 | return error.UnsupportedTransferEncoding; | ||
| 449 | } | 440 | } |
| 450 | } else { | 441 | } else { |
| 451 | res.transfer_encoding = .none; | 442 | if (has_content_length) { |
| 443 | const content_length = std.fmt.parseInt(u64, res.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; | ||
| 444 | |||
| 445 | res.transfer_encoding = .{ .content_length = content_length }; | ||
| 446 | } else if (has_transfer_encoding) { | ||
| 447 | const transfer_encoding = res.headers.getFirstValue("transfer-encoding").?; | ||
| 448 | if (std.mem.eql(u8, transfer_encoding, "chunked")) { | ||
| 449 | res.transfer_encoding = .chunked; | ||
| 450 | } else { | ||
| 451 | return error.UnsupportedTransferEncoding; | ||
| 452 | } | ||
| 453 | } else { | ||
| 454 | res.transfer_encoding = .none; | ||
| 455 | } | ||
| 452 | } | 456 | } |
| 453 | } | ||
| 454 | 457 | ||
| 455 | try w.print("{}", .{res.headers}); | 458 | try w.print("{}", .{res.headers}); |
| 459 | } | ||
| 456 | 460 | ||
| 457 | try w.writeAll("\r\n"); | 461 | try w.writeAll("\r\n"); |
| 458 | 462 | ||
| ... | @@ -516,6 +520,10 @@ pub const Response = struct { | ... | @@ -516,6 +520,10 @@ pub const Response = struct { |
| 516 | res.request.parser.done = true; | 520 | res.request.parser.done = true; |
| 517 | } | 521 | } |
| 518 | 522 | ||
| 523 | if (res.request.method == .HEAD) { | ||
| 524 | res.request.parser.done = true; | ||
| 525 | } | ||
| 526 | |||
| 519 | if (!res.request.parser.done) { | 527 | if (!res.request.parser.done) { |
| 520 | if (res.request.transfer_compression) |tc| switch (tc) { | 528 | if (res.request.transfer_compression) |tc| switch (tc) { |
| 521 | .compress => return error.CompressionNotSupported, | 529 | .compress => return error.CompressionNotSupported, |
lib/std/http/protocol.zig+6-3| ... | @@ -534,9 +534,9 @@ pub const HeadersParser = struct { | ... | @@ -534,9 +534,9 @@ pub const HeadersParser = struct { |
| 534 | 534 | ||
| 535 | if (r.next_chunk_length == 0) r.done = true; | 535 | if (r.next_chunk_length == 0) r.done = true; |
| 536 | 536 | ||
| 537 | return 0; | 537 | return out_index; |
| 538 | } else { | 538 | } else if (out_index < buffer.len) { |
| 539 | const out_avail = buffer.len; | 539 | const out_avail = buffer.len - out_index; |
| 540 | 540 | ||
| 541 | const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); | 541 | const can_read = @as(usize, @intCast(@min(data_avail, out_avail))); |
| 542 | const nread = try conn.read(buffer[0..can_read]); | 542 | const nread = try conn.read(buffer[0..can_read]); |
| ... | @@ -545,6 +545,8 @@ pub const HeadersParser = struct { | ... | @@ -545,6 +545,8 @@ pub const HeadersParser = struct { |
| 545 | if (r.next_chunk_length == 0) r.done = true; | 545 | if (r.next_chunk_length == 0) r.done = true; |
| 546 | 546 | ||
| 547 | return nread; | 547 | return nread; |
| 548 | } else { | ||
| 549 | return out_index; | ||
| 548 | } | 550 | } |
| 549 | }, | 551 | }, |
| 550 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { | 552 | .chunk_data_suffix, .chunk_data_suffix_r, .chunk_head_size, .chunk_head_ext, .chunk_head_r => { |
| ... | @@ -558,6 +560,7 @@ pub const HeadersParser = struct { | ... | @@ -558,6 +560,7 @@ pub const HeadersParser = struct { |
| 558 | .chunk_data => if (r.next_chunk_length == 0) { | 560 | .chunk_data => if (r.next_chunk_length == 0) { |
| 559 | if (std.mem.eql(u8, conn.peek(), "\r\n")) { | 561 | if (std.mem.eql(u8, conn.peek(), "\r\n")) { |
| 560 | r.state = .finished; | 562 | r.state = .finished; |
| 563 | r.done = true; | ||
| 561 | } else { | 564 | } else { |
| 562 | // The trailer section is formatted identically to the header section. | 565 | // The trailer section is formatted identically to the header section. |
| 563 | r.state = .seen_rn; | 566 | r.state = .seen_rn; |
test/standalone/http.zig+69-1| ... | @@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void { | ... | @@ -22,6 +22,18 @@ fn handleRequest(res: *Server.Response) !void { |
| 22 | 22 | ||
| 23 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); | 23 | log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); |
| 24 | 24 | ||
| 25 | if (res.request.headers.contains("expect")) { | ||
| 26 | if (mem.eql(u8, res.request.headers.getFirstValue("expect").?, "100-continue")) { | ||
| 27 | res.status = .@"continue"; | ||
| 28 | try res.do(); | ||
| 29 | res.status = .ok; | ||
| 30 | } else { | ||
| 31 | res.status = .expectation_failed; | ||
| 32 | try res.do(); | ||
| 33 | return; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 25 | const body = try res.reader().readAllAlloc(salloc, 8192); | 37 | const body = try res.reader().readAllAlloc(salloc, 8192); |
| 26 | defer salloc.free(body); | 38 | defer salloc.free(body); |
| 27 | 39 | ||
| ... | @@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void { | ... | @@ -62,7 +74,7 @@ fn handleRequest(res: *Server.Response) !void { |
| 62 | } | 74 | } |
| 63 | 75 | ||
| 64 | try res.finish(); | 76 | try res.finish(); |
| 65 | } else if (mem.eql(u8, res.request.target, "/echo-content")) { | 77 | } else if (mem.startsWith(u8, res.request.target, "/echo-content")) { |
| 66 | try testing.expectEqualStrings("Hello, World!\n", body); | 78 | try testing.expectEqualStrings("Hello, World!\n", body); |
| 67 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); | 79 | try testing.expectEqualStrings("text/plain", res.request.headers.getFirstValue("content-type").?); |
| 68 | 80 | ||
| ... | @@ -592,6 +604,62 @@ pub fn main() !void { | ... | @@ -592,6 +604,62 @@ pub fn main() !void { |
| 592 | try testing.expectEqualStrings("Hello, World!\n", res.body.?); | 604 | try testing.expectEqualStrings("Hello, World!\n", res.body.?); |
| 593 | } | 605 | } |
| 594 | 606 | ||
| 607 | { // expect: 100-continue | ||
| 608 | var h = http.Headers{ .allocator = calloc }; | ||
| 609 | defer h.deinit(); | ||
| 610 | |||
| 611 | try h.append("expect", "100-continue"); | ||
| 612 | try h.append("content-type", "text/plain"); | ||
| 613 | |||
| 614 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-100", .{port}); | ||
| 615 | defer calloc.free(location); | ||
| 616 | const uri = try std.Uri.parse(location); | ||
| 617 | |||
| 618 | log.info("{s}", .{location}); | ||
| 619 | var req = try client.request(.POST, uri, h, .{}); | ||
| 620 | defer req.deinit(); | ||
| 621 | |||
| 622 | req.transfer_encoding = .chunked; | ||
| 623 | |||
| 624 | try req.start(); | ||
| 625 | try req.wait(); | ||
| 626 | try testing.expectEqual(http.Status.@"continue", req.response.status); | ||
| 627 | |||
| 628 | try req.writeAll("Hello, "); | ||
| 629 | try req.writeAll("World!\n"); | ||
| 630 | try req.finish(); | ||
| 631 | |||
| 632 | try req.wait(); | ||
| 633 | try testing.expectEqual(http.Status.ok, req.response.status); | ||
| 634 | |||
| 635 | const body = try req.reader().readAllAlloc(calloc, 8192); | ||
| 636 | defer calloc.free(body); | ||
| 637 | |||
| 638 | try testing.expectEqualStrings("Hello, World!\n", body); | ||
| 639 | } | ||
| 640 | |||
| 641 | { // expect: garbage | ||
| 642 | var h = http.Headers{ .allocator = calloc }; | ||
| 643 | defer h.deinit(); | ||
| 644 | |||
| 645 | try h.append("content-type", "text/plain"); | ||
| 646 | try h.append("expect", "garbage"); | ||
| 647 | |||
| 648 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/echo-content#expect-garbage", .{port}); | ||
| 649 | defer calloc.free(location); | ||
| 650 | const uri = try std.Uri.parse(location); | ||
| 651 | |||
| 652 | log.info("{s}", .{location}); | ||
| 653 | var req = try client.request(.POST, uri, h, .{}); | ||
| 654 | defer req.deinit(); | ||
| 655 | |||
| 656 | req.transfer_encoding = .chunked; | ||
| 657 | |||
| 658 | try req.start(); | ||
| 659 | try req.wait(); | ||
| 660 | try testing.expectEqual(http.Status.expectation_failed, req.response.status); | ||
| 661 | } | ||
| 662 | |||
| 595 | { // issue 16282 *** This test leaves the client in an invalid state, it must be last *** | 663 | { // issue 16282 *** This test leaves the client in an invalid state, it must be last *** |
| 596 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); | 664 | const location = try std.fmt.allocPrint(calloc, "http://127.0.0.1:{d}/get", .{port}); |
| 597 | defer calloc.free(location); | 665 | defer calloc.free(location); |