Class: Async::HTTP::Middleware::LocationRedirector

Inherits:
Protocol::HTTP::Middleware
  • Object
show all
Defined in:
lib/async/http/middleware/location_redirector.rb

Overview

A client wrapper which transparently handles redirects to a given maximum number of hops.

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.

The best reference for these semantics is defined by the [Fetch specification](fetch.spec.whatwg.org/#http-redirect-fetch).

| Redirect using GET | Permanent | Temporary | |:—————————————–:|:———:|:———:| | Allowed | 301 | 302 | | Preserve original method | 308 | 307 |

For the specific details of the redirect handling, see:

Defined Under Namespace

Classes: TooManyRedirects

Constant Summary collapse

PROHIBITED_GET_HEADERS =

Header keys which should be deleted when changing a request from a POST to a GET as defined by <fetch.spec.whatwg.org/#request-body-header-name>.

[
  "content-encoding",
  "content-language",
  "content-location",
  "content-type",
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app, maximum_hops = 3) ⇒ LocationRedirector

maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.



43
44
45
46
47
# File 'lib/async/http/middleware/location_redirector.rb', line 43

def initialize(app, maximum_hops = 3)
  super(app)
  
  @maximum_hops = maximum_hops
end

Instance Attribute Details

#maximum_hopsObject (readonly)

The maximum number of hops which will limit the number of redirects until an error is thrown.



50
51
52
# File 'lib/async/http/middleware/location_redirector.rb', line 50

def maximum_hops
  @maximum_hops
end

Instance Method Details

#call(request) ⇒ Object

Raises:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/async/http/middleware/location_redirector.rb', line 79

def call(request)
  # We don't want to follow redirects for HEAD requests:
  return super if request.head?
  
  body = ::Protocol::HTTP::Body::Rewindable.wrap(request)
  hops = 0
  
  while hops <= @maximum_hops
    response = super(request)
    
    if response.redirection?
      hops += 1
      
      # Get the redirect location:
      unless location = response.headers["location"]
        return response
      end
      
      response.finish
      
      unless handle_redirect(request, location)
        return response
      end
      
      # Ensure the request (body) is finished and set to nil before we manipulate the request:
      request.finish
      
      if request.method == GET or response.preserve_method?
        # We (might) need to rewind the body so that it can be submitted again:
        body&.rewind
        request.body = body
      else
        # We are changing the method to GET:
        request.method = GET
        
        # We will no longer be submitting the body:
        body = nil
        
        # Remove any headers which are not allowed in a GET request:
        PROHIBITED_GET_HEADERS.each do |header|
          request.headers.delete(header)
        end
      end
    else
      return response
    end
  end
  
  raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
end

#handle_redirect(request, location) ⇒ Object

Handle a redirect to a relative location.



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/async/http/middleware/location_redirector.rb', line 65

def handle_redirect(request, location)
  uri = URI.parse(location)
  
  if uri.absolute?
    return false
  end
  
  # Update the path of the request:
  request.path = ::Protocol::URL::Reference[request.path] + location
  
  # Follow the redirect:
  return true
end

#redirect_with_get?(request, response) ⇒ Boolean

Returns:

  • (Boolean)


52
53
54
55
56
57
58
# File 'lib/async/http/middleware/location_redirector.rb', line 52

def redirect_with_get?(request, response)
  # We only want to switch to GET if the request method is something other than get, e.g. POST.
  if request.method != GET
    # According to the RFC, we should only switch to GET if the response is a 301 or 302:
    return response.status == 301 || response.status == 302
  end
end