Skip to content

Commit b0795d7

Browse files
committed
Move Async::HTTP::RelativeLocation to Async::HTTP::Middleware::LocationRedirector.
1 parent 11b9d5d commit b0795d7

File tree

3 files changed

+153
-133
lines changed

3 files changed

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

lib/async/http/relative_location.rb

Lines changed: 5 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -4,141 +4,15 @@
44
# Copyright, 2018-2023, by Samuel Williams.
55
# Copyright, 2019-2020, by Brian Morearty.
66

7-
require_relative 'client'
8-
require_relative 'endpoint'
9-
require_relative 'reference'
7+
require_relative 'middleware/location_redirector'
108

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
1310

1411
module Async
1512
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
14216
end
14317
end
14418
end

test/async/http/relative_location.rb renamed to test/async/http/middleware/location_redirector.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
# Copyright, 2018-2023, by Samuel Williams.
55
# Copyright, 2019-2020, by Brian Morearty.
66

7-
require 'async/http/relative_location'
7+
require 'async/http/middleware/location_redirector'
88
require 'async/http/server'
99

1010
require 'sus/fixtures/async/http'
1111

12-
describe Async::HTTP::RelativeLocation do
12+
describe Async::HTTP::Middleware::LocationRedirector do
1313
include Sus::Fixtures::Async::HTTP::ServerContext
1414

1515
let(:relative_location) {subject.new(@client, 1)}

0 commit comments

Comments
 (0)