23
23
require_relative 'reference'
24
24
25
25
require 'protocol/http/middleware'
26
+ require 'protocol/http/body/rewindable'
26
27
27
28
module Async
28
29
module HTTP
29
30
class TooManyRedirects < StandardError
30
31
end
31
32
32
33
# 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
+ #
33
48
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
+ ]
35
56
36
57
# maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
37
58
def initialize ( app , maximum_hops = 3 )
@@ -43,20 +64,39 @@ def initialize(app, maximum_hops = 3)
43
64
# The maximum number of hops which will limit the number of redirects until an error is thrown.
44
65
attr :maximum_hops
45
66
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
+
46
75
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
48
84
49
- # We need to cache the body as it might be submitted multiple times.
50
- request . finish
85
+ hops = 0
51
86
52
87
while hops <= @maximum_hops
53
88
response = super ( request )
54
89
55
90
if response . redirection?
56
91
hops += 1
92
+
93
+ # Get the redirect location:
94
+ unless location = response . headers [ 'location' ]
95
+ return response
96
+ end
97
+
57
98
response . finish
58
99
59
- location = response . headers [ 'location' ]
60
100
uri = URI . parse ( location )
61
101
62
102
if uri . absolute?
@@ -65,8 +105,21 @@ def call(request)
65
105
request . path = Reference [ request . path ] + location
66
106
end
67
107
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
70
123
end
71
124
else
72
125
return response
0 commit comments