| ... | @@ -580,11 +580,7 @@ pub const Request = struct { | ... | @@ -580,11 +580,7 @@ pub const Request = struct { |
| 580 | /// The transfer encoding of the request body. | 580 | /// The transfer encoding of the request body. |
| 581 | transfer_encoding: RequestTransfer = .none, | 581 | transfer_encoding: RequestTransfer = .none, |
| 582 | | 582 | |
| 583 | /// The redirect quota left for this request. | 583 | redirect_behavior: RedirectBehavior, |
| 584 | redirects_left: u32, | | |
| 585 | | | |
| 586 | /// Whether the request should follow redirects. | | |
| 587 | handle_redirects: bool, | | |
| 588 | | 584 | |
| 589 | /// Whether the request should handle a 100-continue response before sending the request body. | 585 | /// Whether the request should handle a 100-continue response before sending the request body. |
| 590 | handle_continue: bool, | 586 | handle_continue: bool, |
| ... | @@ -597,6 +593,25 @@ pub const Request = struct { | ... | @@ -597,6 +593,25 @@ pub const Request = struct { |
| 597 | /// Used as a allocator for resolving redirects locations. | 593 | /// Used as a allocator for resolving redirects locations. |
| 598 | arena: std.heap.ArenaAllocator, | 594 | arena: std.heap.ArenaAllocator, |
| 599 | | 595 | |
| | 596 | /// Any value other than `not_allowed` or `unhandled` means that integer represents |
| | 597 | /// how many remaining redirects are allowed. |
| | 598 | pub const RedirectBehavior = enum(u16) { |
| | 599 | /// The next redirect will cause an error. |
| | 600 | not_allowed = 0, |
| | 601 | /// Redirects are passed to the client to analyze the redirect response |
| | 602 | /// directly. |
| | 603 | unhandled = std.math.maxInt(u16), |
| | 604 | _, |
| | 605 | |
| | 606 | pub fn subtractOne(rb: *RedirectBehavior) void { |
| | 607 | switch (rb.*) { |
| | 608 | .not_allowed => unreachable, |
| | 609 | .unhandled => unreachable, |
| | 610 | _ => rb.* = @enumFromInt(@intFromEnum(rb.*) - 1), |
| | 611 | } |
| | 612 | } |
| | 613 | }; |
| | 614 | |
| 600 | /// Frees all resources associated with the request. | 615 | /// Frees all resources associated with the request. |
| 601 | pub fn deinit(req: *Request) void { | 616 | pub fn deinit(req: *Request) void { |
| 602 | switch (req.response.compression) { | 617 | switch (req.response.compression) { |
| ... | @@ -621,8 +636,9 @@ pub const Request = struct { | ... | @@ -621,8 +636,9 @@ pub const Request = struct { |
| 621 | req.* = undefined; | 636 | req.* = undefined; |
| 622 | } | 637 | } |
| 623 | | 638 | |
| 624 | // This function must deallocate all resources associated with the request, or keep those which will be used | 639 | // This function must deallocate all resources associated with the request, |
| 625 | // This needs to be kept in sync with deinit and request | 640 | // or keep those which will be used. |
| | 641 | // This needs to be kept in sync with deinit and request. |
| 626 | fn redirect(req: *Request, uri: Uri) !void { | 642 | fn redirect(req: *Request, uri: Uri) !void { |
| 627 | assert(req.response.parser.state == .complete); | 643 | assert(req.response.parser.state == .complete); |
| 628 | | 644 | |
| ... | @@ -647,7 +663,7 @@ pub const Request = struct { | ... | @@ -647,7 +663,7 @@ pub const Request = struct { |
| 647 | | 663 | |
| 648 | req.uri = uri; | 664 | req.uri = uri; |
| 649 | req.connection = try req.client.connect(host, port, protocol); | 665 | req.connection = try req.client.connect(host, port, protocol); |
| 650 | req.redirects_left -= 1; | 666 | req.redirect_behavior.subtractOne(); |
| 651 | req.response.headers.clearRetainingCapacity(); | 667 | req.response.headers.clearRetainingCapacity(); |
| 652 | req.response.parser.reset(); | 668 | req.response.parser.reset(); |
| 653 | | 669 | |
| ... | @@ -819,7 +835,7 @@ pub const Request = struct { | ... | @@ -819,7 +835,7 @@ pub const Request = struct { |
| 819 | /// Waits for a response from the server and parses any headers that are sent. | 835 | /// Waits for a response from the server and parses any headers that are sent. |
| 820 | /// This function will block until the final response is received. | 836 | /// This function will block until the final response is received. |
| 821 | /// | 837 | /// |
| 822 | /// If `handle_redirects` is true and the request has no payload, then this | 838 | /// If handling redirects and the request has no payload, then this |
| 823 | /// function will automatically follow redirects. If a request payload is | 839 | /// function will automatically follow redirects. If a request payload is |
| 824 | /// present, then this function will error with | 840 | /// present, then this function will error with |
| 825 | /// error.RedirectRequiresResend. | 841 | /// error.RedirectRequiresResend. |
| ... | @@ -897,15 +913,14 @@ pub const Request = struct { | ... | @@ -897,15 +913,14 @@ pub const Request = struct { |
| 897 | req.response.parser.next_chunk_length = std.math.maxInt(u64); | 913 | req.response.parser.next_chunk_length = std.math.maxInt(u64); |
| 898 | } | 914 | } |
| 899 | | 915 | |
| 900 | if (req.response.status.class() == .redirect and req.handle_redirects) { | 916 | if (req.response.status.class() == .redirect and req.redirect_behavior != .unhandled) { |
| 901 | req.response.skip = true; | 917 | req.response.skip = true; |
| 902 | | 918 | |
| 903 | // skip the body of the redirect response, this will at least | 919 | // skip the body of the redirect response, this will at least |
| 904 | // leave the connection in a known good state. | 920 | // leave the connection in a known good state. |
| 905 | const empty = @as([*]u8, undefined)[0..0]; | 921 | assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary |
| 906 | assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary | | |
| 907 | | 922 | |
| 908 | if (req.redirects_left == 0) return error.TooManyHttpRedirects; | 923 | if (req.redirect_behavior == .not_allowed) return error.TooManyHttpRedirects; |
| 909 | | 924 | |
| 910 | const location = req.response.headers.getFirstValue("location") orelse | 925 | const location = req.response.headers.getFirstValue("location") orelse |
| 911 | return error.HttpRedirectMissingLocation; | 926 | return error.HttpRedirectMissingLocation; |
| ... | @@ -1380,7 +1395,7 @@ pub fn connectTunnel( | ... | @@ -1380,7 +1395,7 @@ pub fn connectTunnel( |
| 1380 | | 1395 | |
| 1381 | var buffer: [8096]u8 = undefined; | 1396 | var buffer: [8096]u8 = undefined; |
| 1382 | var req = client.open(.CONNECT, uri, proxy.headers, .{ | 1397 | var req = client.open(.CONNECT, uri, proxy.headers, .{ |
| 1383 | .handle_redirects = false, | 1398 | .redirect_behavior = .unhandled, |
| 1384 | .connection = conn, | 1399 | .connection = conn, |
| 1385 | .server_header_buffer = &buffer, | 1400 | .server_header_buffer = &buffer, |
| 1386 | }) catch |err| { | 1401 | }) catch |err| { |
| ... | @@ -1481,13 +1496,13 @@ pub const RequestOptions = struct { | ... | @@ -1481,13 +1496,13 @@ pub const RequestOptions = struct { |
| 1481 | /// you finish the request, then the request *will* deadlock. | 1496 | /// you finish the request, then the request *will* deadlock. |
| 1482 | handle_continue: bool = true, | 1497 | handle_continue: bool = true, |
| 1483 | | 1498 | |
| 1484 | /// Automatically follow redirects. This will only follow redirects for | 1499 | /// This field specifies whether to automatically follow redirects, and if |
| 1485 | /// repeatable requests (ie. with no payload or the server has acknowledged | 1500 | /// so, how many redirects to follow before returning an error. |
| 1486 | /// the payload). | 1501 | /// |
| 1487 | handle_redirects: bool = true, | 1502 | /// This will only follow redirects for repeatable requests (ie. with no |
| | 1503 | /// payload or the server has acknowledged the payload). |
| | 1504 | redirect_behavior: Request.RedirectBehavior = @enumFromInt(3), |
| 1488 | | 1505 | |
| 1489 | /// How many redirects to follow before returning an error. | | |
| 1490 | max_redirects: u32 = 3, | | |
| 1491 | /// Externally-owned memory used to store the server's entire HTTP header. | 1506 | /// Externally-owned memory used to store the server's entire HTTP header. |
| 1492 | /// `error.HttpHeadersOversize` is returned from read() when a | 1507 | /// `error.HttpHeadersOversize` is returned from read() when a |
| 1493 | /// client sends too many bytes of HTTP headers. | 1508 | /// client sends too many bytes of HTTP headers. |
| ... | @@ -1548,8 +1563,7 @@ pub fn open( | ... | @@ -1548,8 +1563,7 @@ pub fn open( |
| 1548 | .headers = try headers.clone(client.allocator), // Headers must be cloned to properly handle header transformations in redirects. | 1563 | .headers = try headers.clone(client.allocator), // Headers must be cloned to properly handle header transformations in redirects. |
| 1549 | .method = method, | 1564 | .method = method, |
| 1550 | .version = options.version, | 1565 | .version = options.version, |
| 1551 | .redirects_left = options.max_redirects, | 1566 | .redirect_behavior = options.redirect_behavior, |
| 1552 | .handle_redirects = options.handle_redirects, | | |
| 1553 | .handle_continue = options.handle_continue, | 1567 | .handle_continue = options.handle_continue, |
| 1554 | .response = .{ | 1568 | .response = .{ |
| 1555 | .status = undefined, | 1569 | .status = undefined, |
| ... | @@ -1600,6 +1614,7 @@ pub const FetchOptions = struct { | ... | @@ -1600,6 +1614,7 @@ pub const FetchOptions = struct { |
| 1600 | | 1614 | |
| 1601 | server_header_buffer: ?[]u8 = null, | 1615 | server_header_buffer: ?[]u8 = null, |
| 1602 | response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } }, | 1616 | response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } }, |
| | 1617 | redirect_behavior: ?Request.RedirectBehavior = null, |
| 1603 | | 1618 | |
| 1604 | location: Location, | 1619 | location: Location, |
| 1605 | method: http.Method = .GET, | 1620 | method: http.Method = .GET, |
| ... | @@ -1642,7 +1657,8 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc | ... | @@ -1642,7 +1657,8 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc |
| 1642 | | 1657 | |
| 1643 | var req = try open(client, options.method, uri, options.headers, .{ | 1658 | var req = try open(client, options.method, uri, options.headers, .{ |
| 1644 | .server_header_buffer = options.server_header_buffer orelse &server_header_buffer, | 1659 | .server_header_buffer = options.server_header_buffer orelse &server_header_buffer, |
| 1645 | .handle_redirects = options.payload == .none, | 1660 | .redirect_behavior = options.redirect_behavior orelse |
| | 1661 | if (options.payload == .none) @enumFromInt(3) else .unhandled, |
| 1646 | }); | 1662 | }); |
| 1647 | defer req.deinit(); | 1663 | defer req.deinit(); |
| 1648 | | 1664 | |
| ... | @@ -1694,8 +1710,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc | ... | @@ -1694,8 +1710,7 @@ pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !Fetc |
| 1694 | .none => { // Take advantage of request internals to discard the response body and make the connection available for another request. | 1710 | .none => { // Take advantage of request internals to discard the response body and make the connection available for another request. |
| 1695 | req.response.skip = true; | 1711 | req.response.skip = true; |
| 1696 | | 1712 | |
| 1697 | const empty = @as([*]u8, undefined)[0..0]; | 1713 | assert(try req.transferRead(&.{}) == 0); // we're skipping, no buffer is necessary |
| 1698 | assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary | | |
| 1699 | }, | 1714 | }, |
| 1700 | } | 1715 | } |
| 1701 | | 1716 | |