| ... | @@ -365,7 +365,7 @@ pub const Response = struct { | ... | @@ -365,7 +365,7 @@ pub const Response = struct { |
| 365 | CompressionNotSupported, | 365 | CompressionNotSupported, |
| 366 | }; | 366 | }; |
| 367 | | 367 | |
| 368 | pub fn parse(res: *Response, bytes: []const u8) ParseError!void { | 368 | pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void { |
| 369 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); | 369 | var it = mem.tokenize(u8, bytes[0 .. bytes.len - 4], "\r\n"); |
| 370 | | 370 | |
| 371 | const first_line = it.next() orelse return error.HttpHeadersInvalid; | 371 | const first_line = it.next() orelse return error.HttpHeadersInvalid; |
| ... | @@ -398,6 +398,8 @@ pub const Response = struct { | ... | @@ -398,6 +398,8 @@ pub const Response = struct { |
| 398 | | 398 | |
| 399 | try res.headers.append(header_name, header_value); | 399 | try res.headers.append(header_name, header_value); |
| 400 | | 400 | |
| | 401 | if (trailing) continue; |
| | 402 | |
| 401 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { | 403 | if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { |
| 402 | if (res.content_length != null) return error.HttpHeadersInvalid; | 404 | if (res.content_length != null) return error.HttpHeadersInvalid; |
| 403 | res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; | 405 | res.content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; |
| ... | @@ -480,7 +482,7 @@ pub const Response = struct { | ... | @@ -480,7 +482,7 @@ pub const Response = struct { |
| 480 | | 482 | |
| 481 | /// A HTTP request that has been sent. | 483 | /// A HTTP request that has been sent. |
| 482 | /// | 484 | /// |
| 483 | /// Order of operations: request[ -> write -> finish] -> do -> read | 485 | /// Order of operations: request -> start[ -> write -> finish] -> wait -> read |
| 484 | pub const Request = struct { | 486 | pub const Request = struct { |
| 485 | uri: Uri, | 487 | uri: Uri, |
| 486 | client: *Client, | 488 | client: *Client, |
| ... | @@ -508,8 +510,9 @@ pub const Request = struct { | ... | @@ -508,8 +510,9 @@ pub const Request = struct { |
| 508 | .zstd => |*zstd| zstd.deinit(), | 510 | .zstd => |*zstd| zstd.deinit(), |
| 509 | } | 511 | } |
| 510 | | 512 | |
| | 513 | req.response.headers.deinit(); |
| | 514 | |
| 511 | if (req.response.parser.header_bytes_owned) { | 515 | if (req.response.parser.header_bytes_owned) { |
| 512 | req.response.headers.deinit(); | | |
| 513 | req.response.parser.header_bytes.deinit(req.client.allocator); | 516 | req.response.parser.header_bytes.deinit(req.client.allocator); |
| 514 | } | 517 | } |
| 515 | | 518 | |
| ... | @@ -524,6 +527,44 @@ pub const Request = struct { | ... | @@ -524,6 +527,44 @@ pub const Request = struct { |
| 524 | req.* = undefined; | 527 | req.* = undefined; |
| 525 | } | 528 | } |
| 526 | | 529 | |
| | 530 | // This function must deallocate all resources associated with the request, or keep those which will be used |
| | 531 | // This needs to be kept in sync with deinit and request |
| | 532 | fn redirect(req: *Request, uri: Uri) !void { |
| | 533 | assert(req.response.parser.done); |
| | 534 | |
| | 535 | switch (req.response.compression) { |
| | 536 | .none => {}, |
| | 537 | .deflate => |*deflate| deflate.deinit(), |
| | 538 | .gzip => |*gzip| gzip.deinit(), |
| | 539 | .zstd => |*zstd| zstd.deinit(), |
| | 540 | } |
| | 541 | |
| | 542 | req.client.connection_pool.release(req.client, req.connection); |
| | 543 | |
| | 544 | const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; |
| | 545 | |
| | 546 | const port: u16 = uri.port orelse switch (protocol) { |
| | 547 | .plain => 80, |
| | 548 | .tls => 443, |
| | 549 | }; |
| | 550 | |
| | 551 | const host = uri.host orelse return error.UriMissingHost; |
| | 552 | |
| | 553 | req.uri = uri; |
| | 554 | req.connection = try req.client.connect(host, port, protocol); |
| | 555 | req.redirects_left -= 1; |
| | 556 | req.response.headers.clearRetainingCapacity(); |
| | 557 | req.response.parser.reset(); |
| | 558 | |
| | 559 | req.response = .{ |
| | 560 | .status = undefined, |
| | 561 | .reason = undefined, |
| | 562 | .version = undefined, |
| | 563 | .headers = req.response.headers, |
| | 564 | .parser = req.response.parser, |
| | 565 | }; |
| | 566 | } |
| | 567 | |
| 527 | pub const StartError = BufferedConnection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; | 568 | pub const StartError = BufferedConnection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; |
| 528 | | 569 | |
| 529 | /// Send the request to the server. | 570 | /// Send the request to the server. |
| ... | @@ -627,14 +668,14 @@ pub const Request = struct { | ... | @@ -627,14 +668,14 @@ pub const Request = struct { |
| 627 | return index; | 668 | return index; |
| 628 | } | 669 | } |
| 629 | | 670 | |
| 630 | pub const DoError = RequestError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported }; | 671 | pub const WaitError = RequestError || StartError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, CannotRedirect, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported }; |
| 631 | | 672 | |
| 632 | /// Waits for a response from the server and parses any headers that are sent. | 673 | /// Waits for a response from the server and parses any headers that are sent. |
| 633 | /// This function will block until the final response is received. | 674 | /// This function will block until the final response is received. |
| 634 | /// | 675 | /// |
| 635 | /// If `handle_redirects` is true, then this function will automatically follow | 676 | /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow |
| 636 | /// redirects. | 677 | /// redirects. If a request payload is present, then this function will error with error.CannotRedirect. |
| 637 | pub fn do(req: *Request) DoError!void { | 678 | pub fn wait(req: *Request) WaitError!void { |
| 638 | while (true) { // handle redirects | 679 | while (true) { // handle redirects |
| 639 | while (true) { // read headers | 680 | while (true) { // read headers |
| 640 | try req.connection.data.buffered.fill(); | 681 | try req.connection.data.buffered.fill(); |
| ... | @@ -645,7 +686,7 @@ pub const Request = struct { | ... | @@ -645,7 +686,7 @@ pub const Request = struct { |
| 645 | if (req.response.parser.state.isContent()) break; | 686 | if (req.response.parser.state.isContent()) break; |
| 646 | } | 687 | } |
| 647 | | 688 | |
| 648 | try req.response.parse(req.response.parser.header_bytes.items); | 689 | try req.response.parse(req.response.parser.header_bytes.items, false); |
| 649 | | 690 | |
| 650 | if (req.response.status == .switching_protocols) { | 691 | if (req.response.status == .switching_protocols) { |
| 651 | req.connection.data.closing = false; | 692 | req.connection.data.closing = false; |
| ... | @@ -684,7 +725,7 @@ pub const Request = struct { | ... | @@ -684,7 +725,7 @@ pub const Request = struct { |
| 684 | req.response.parser.done = true; | 725 | req.response.parser.done = true; |
| 685 | } | 726 | } |
| 686 | | 727 | |
| 687 | if (req.response.status.class() == .redirect and req.handle_redirects) { | 728 | if (req.transfer_encoding == .none and req.response.status.class() == .redirect and req.handle_redirects) { |
| 688 | req.response.skip = true; | 729 | req.response.skip = true; |
| 689 | | 730 | |
| 690 | const empty = @as([*]u8, undefined)[0..0]; | 731 | const empty = @as([*]u8, undefined)[0..0]; |
| ... | @@ -694,26 +735,17 @@ pub const Request = struct { | ... | @@ -694,26 +735,17 @@ pub const Request = struct { |
| 694 | | 735 | |
| 695 | const location = req.response.headers.getFirstValue("location") orelse | 736 | const location = req.response.headers.getFirstValue("location") orelse |
| 696 | return error.HttpRedirectMissingLocation; | 737 | return error.HttpRedirectMissingLocation; |
| 697 | const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); | 738 | |
| 698 | | 739 | const arena = req.arena.allocator(); |
| 699 | var new_arena = std.heap.ArenaAllocator.init(req.client.allocator); | 740 | |
| 700 | const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator()); | 741 | const location_duped = try arena.dupe(u8, location); |
| 701 | errdefer new_arena.deinit(); | 742 | |
| 702 | | 743 | const new_url = Uri.parse(location_duped) catch try Uri.parseWithoutScheme(location_duped); |
| 703 | req.arena.deinit(); | 744 | const resolved_url = try req.uri.resolve(new_url, false, arena); |
| 704 | req.arena = new_arena; | 745 | |
| 705 | | 746 | try req.redirect(resolved_url); |
| 706 | const new_req = try req.client.request(req.method, resolved_url, req.headers, .{ | 747 | |
| 707 | .version = req.version, | 748 | try req.start(); |
| 708 | .max_redirects = req.redirects_left - 1, | | |
| 709 | .header_strategy = if (req.response.parser.header_bytes_owned) .{ | | |
| 710 | .dynamic = req.response.parser.max_header_bytes, | | |
| 711 | } else .{ | | |
| 712 | .static = req.response.parser.header_bytes.items.ptr[0..req.response.parser.max_header_bytes], | | |
| 713 | }, | | |
| 714 | }); | | |
| 715 | req.deinit(); | | |
| 716 | req.* = new_req; | | |
| 717 | } else { | 749 | } else { |
| 718 | req.response.skip = false; | 750 | req.response.skip = false; |
| 719 | if (!req.response.parser.done) { | 751 | if (!req.response.parser.done) { |
| ... | @@ -731,6 +763,9 @@ pub const Request = struct { | ... | @@ -731,6 +763,9 @@ pub const Request = struct { |
| 731 | }; | 763 | }; |
| 732 | } | 764 | } |
| 733 | | 765 | |
| | 766 | if (req.response.status.class() == .redirect and req.handle_redirects and req.transfer_encoding != .none) |
| | 767 | return error.CannotRedirect; // The request body has already been sent. The request is still in a valid state, but the redirect must be handled manually. |
| | 768 | |
| 734 | break; | 769 | break; |
| 735 | } | 770 | } |
| 736 | } | 771 | } |
| ... | @@ -768,7 +803,7 @@ pub const Request = struct { | ... | @@ -768,7 +803,7 @@ pub const Request = struct { |
| 768 | | 803 | |
| 769 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. | 804 | // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. |
| 770 | // This will *only* fail for a malformed trailer. | 805 | // This will *only* fail for a malformed trailer. |
| 771 | req.response.parse(req.response.parser.header_bytes.items) catch return error.InvalidTrailers; | 806 | req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers; |
| 772 | } | 807 | } |
| 773 | } | 808 | } |
| 774 | | 809 | |