|
4 | 4 | # Copyright, 2018-2023, by Samuel Williams.
|
5 | 5 | # Copyright, 2019-2020, by Brian Morearty.
|
6 | 6 |
|
7 |
| -require_relative 'client' |
8 |
| -require_relative 'endpoint' |
9 |
| -require_relative 'reference' |
| 7 | +require_relative 'middleware/location_redirector' |
10 | 8 |
|
11 |
| -require 'protocol/http/middleware' |
12 |
| -require 'protocol/http/body/rewindable' |
| 9 | +warn "`Async::HTTP::RelativeLocation` is deprecated and will be removed in the next release. Please use `Async::HTTP::Middleware::LocationRedirector` instead.", uplevel: 1 |
13 | 10 |
|
14 | 11 | module Async
|
15 | 12 | module HTTP
|
16 |
| - class TooManyRedirects < StandardError |
17 |
| - end |
18 |
| - |
19 |
| - # A client wrapper which transparently handles redirects to a given maximum number of hops. |
20 |
| - # |
21 |
| - # The default implementation will only follow relative locations (i.e. those without a scheme) and will switch to GET if the original request was not a GET. |
22 |
| - # |
23 |
| - # The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch). |
24 |
| - # |
25 |
| - # | Redirect using GET | Permanent | Temporary | |
26 |
| - # |:-----------------------------------------:|:---------:|:---------:| |
27 |
| - # | Allowed | 301 | 302 | |
28 |
| - # | Preserve original method | 308 | 307 | |
29 |
| - # |
30 |
| - # For the specific details of the redirect handling, see: |
31 |
| - # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently. |
32 |
| - # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found. |
33 |
| - # - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect. |
34 |
| - # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect. |
35 |
| - # |
36 |
| - class RelativeLocation < ::Protocol::HTTP::Middleware |
37 |
| - # Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>. |
38 |
| - PROHIBITED_GET_HEADERS = [ |
39 |
| - 'content-encoding', |
40 |
| - 'content-language', |
41 |
| - 'content-location', |
42 |
| - 'content-type', |
43 |
| - ] |
44 |
| - |
45 |
| - # maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects. |
46 |
| - def initialize(app, maximum_hops = 3) |
47 |
| - super(app) |
48 |
| - |
49 |
| - @maximum_hops = maximum_hops |
50 |
| - end |
51 |
| - |
52 |
| - # The maximum number of hops which will limit the number of redirects until an error is thrown. |
53 |
| - attr :maximum_hops |
54 |
| - |
55 |
| - def redirect_with_get?(request, response) |
56 |
| - # We only want to switch to GET if the request method is something other than get, e.g. POST. |
57 |
| - if request.method != GET |
58 |
| - # According to the RFC, we should only switch to GET if the response is a 301 or 302: |
59 |
| - return response.status == 301 || response.status == 302 |
60 |
| - end |
61 |
| - end |
62 |
| - |
63 |
| - # Handle a redirect to a relative location. |
64 |
| - # |
65 |
| - # @parameter request [Protocol::HTTP::Request] The original request, which you can modify if you want to handle the redirect. |
66 |
| - # @parameter location [String] The relative location to redirect to. |
67 |
| - # @returns [Boolean] True if the redirect was handled, false if it was not. |
68 |
| - def handle_redirect(request, location) |
69 |
| - uri = URI.parse(location) |
70 |
| - |
71 |
| - if uri.absolute? |
72 |
| - return false |
73 |
| - end |
74 |
| - |
75 |
| - # Update the path of the request: |
76 |
| - request.path = Reference[request.path] + location |
77 |
| - |
78 |
| - # Follow the redirect: |
79 |
| - return true |
80 |
| - end |
81 |
| - |
82 |
| - def call(request) |
83 |
| - # We don't want to follow redirects for HEAD requests: |
84 |
| - return super if request.head? |
85 |
| - |
86 |
| - if body = request.body |
87 |
| - if body.respond_to?(:rewind) |
88 |
| - # The request body was already rewindable, so use it as is: |
89 |
| - body = request.body |
90 |
| - else |
91 |
| - # The request body was not rewindable, and we might need to resubmit it if we get a response status of 307 or 308, so make it rewindable: |
92 |
| - body = ::Protocol::HTTP::Body::Rewindable.new(body) |
93 |
| - request.body = body |
94 |
| - end |
95 |
| - end |
96 |
| - |
97 |
| - hops = 0 |
98 |
| - |
99 |
| - while hops <= @maximum_hops |
100 |
| - response = super(request) |
101 |
| - |
102 |
| - if response.redirection? |
103 |
| - hops += 1 |
104 |
| - |
105 |
| - # Get the redirect location: |
106 |
| - unless location = response.headers['location'] |
107 |
| - return response |
108 |
| - end |
109 |
| - |
110 |
| - response.finish |
111 |
| - |
112 |
| - unless handle_redirect(request, location) |
113 |
| - return response |
114 |
| - end |
115 |
| - |
116 |
| - # Ensure the request (body) is finished and set to nil before we manipulate the request: |
117 |
| - request.finish |
118 |
| - |
119 |
| - if request.method == GET or response.preserve_method? |
120 |
| - # We (might) need to rewind the body so that it can be submitted again: |
121 |
| - body&.rewind |
122 |
| - request.body = body |
123 |
| - else |
124 |
| - # We are changing the method to GET: |
125 |
| - request.method = GET |
126 |
| - |
127 |
| - # We will no longer be submitting the body: |
128 |
| - body = nil |
129 |
| - |
130 |
| - # Remove any headers which are not allowed in a GET request: |
131 |
| - PROHIBITED_GET_HEADERS.each do |header| |
132 |
| - request.headers.delete(header) |
133 |
| - end |
134 |
| - end |
135 |
| - else |
136 |
| - return response |
137 |
| - end |
138 |
| - end |
139 |
| - |
140 |
| - raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!" |
141 |
| - end |
| 13 | + module Middleware |
| 14 | + RelativeLocation = Middleware::LocationRedirector |
| 15 | + TooManyRedirects = RelativeLocation::TooManyRedirects |
142 | 16 | end
|
143 | 17 | end
|
144 | 18 | end
|
0 commit comments