2323require_relative 'reference'
2424
2525require 'protocol/http/middleware'
26+ require 'protocol/http/body/rewindable'
2627
2728module 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