Skip to content

Commit 4fb3762

Browse files
authored
Better redirect handling (#110)
* Alignment with fetch specification.
1 parent b333184 commit 4fb3762

File tree

1 file changed

+60
-7
lines changed

1 file changed

+60
-7
lines changed

lib/async/http/relative_location.rb

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,36 @@
2323
require_relative 'reference'
2424

2525
require 'protocol/http/middleware'
26+
require 'protocol/http/body/rewindable'
2627

2728
module Async
2829
module HTTP
2930
class TooManyRedirects < StandardError
3031
end
3132

3233
# A client wrapper which transparently handles both relative and absolute redirects to a given maximum number of hops.
34+
#
35+
# The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch).
36+
#
37+
# | Redirect using GET | Permanent | Temporary |
38+
# |:-----------------------------------------:|:---------:|:---------:|
39+
# | Allowed | 301 | 302 |
40+
# | Preserve original method | 308 | 307 |
41+
#
42+
# For the specific details of the redirect handling, see:
43+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently.
44+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found.
45+
# - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect.
46+
# - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect.
47+
#
3348
class RelativeLocation < ::Protocol::HTTP::Middleware
34-
DEFAULT_METHOD = GET
49+
# 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>.
50+
PROHIBITED_GET_HEADERS = [
51+
'content-encoding',
52+
'content-language',
53+
'content-location',
54+
'content-type',
55+
]
3556

3657
# maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
3758
def initialize(app, maximum_hops = 3)
@@ -43,20 +64,39 @@ def initialize(app, maximum_hops = 3)
4364
# The maximum number of hops which will limit the number of redirects until an error is thrown.
4465
attr :maximum_hops
4566

67+
def redirect_with_get?(request, response)
68+
# We only want to switch to GET if the request method is something other than get, e.g. POST.
69+
if request.method != GET
70+
# According to the RFC, we should only switch to GET if the response is a 301 or 302:
71+
return response.status == 301 || response.status == 302
72+
end
73+
end
74+
4675
def call(request)
47-
hops = 0
76+
# We don't want to follow redirects for HEAD requests:
77+
return super if request.head?
78+
79+
if body = request.body
80+
# We need to cache the body as it might be submitted multiple times if we get a response status of 307 or 308:
81+
body = ::Protocol::HTTP::Body::Rewindable.new(body)
82+
request.body = body
83+
end
4884

49-
# We need to cache the body as it might be submitted multiple times.
50-
request.finish
85+
hops = 0
5186

5287
while hops <= @maximum_hops
5388
response = super(request)
5489

5590
if response.redirection?
5691
hops += 1
92+
93+
# Get the redirect location:
94+
unless location = response.headers['location']
95+
return response
96+
end
97+
5798
response.finish
5899

59-
location = response.headers['location']
60100
uri = URI.parse(location)
61101

62102
if uri.absolute?
@@ -65,8 +105,21 @@ def call(request)
65105
request.path = Reference[request.path] + location
66106
end
67107

68-
unless response.preserve_method?
69-
request.method = DEFAULT_METHOD
108+
if request.method == GET or response.preserve_method?
109+
# We (might) need to rewind the body so that it can be submitted again:
110+
body&.rewind
111+
else
112+
# We are changing the method to GET:
113+
request.method = GET
114+
115+
# Clear the request body:
116+
request.finish
117+
body = nil
118+
119+
# Remove any headers which are not allowed in a GET request:
120+
PROHIBITED_GET_HEADERS.each do |header|
121+
request.headers.delete(header)
122+
end
70123
end
71124
else
72125
return response

0 commit comments

Comments
 (0)