http.rb 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +28 -0
- data/README.md +30 -1
- data/lib/HTTP/REDIRECT.rb +22 -1
- data/lib/HTTP/VERSION.rb +1 -1
- data/lib/HTTP/request.rb +4 -1
- data/test/HTTP/REDIRECT_test.rb +64 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2483f99ec13020c1e1c780b84c7dcddc68437687b1176e7fb88ba228adf084af
|
|
4
|
+
data.tar.gz: 710dd8eed5142ac832553595e581bd2597174dd1131fb14a7c51d4e7f17f4992
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbe519101ddbd07038de07ca7bbcb28bbb2b7c53d0f650df86092433271469da3491326dce2d9c3698f78cb381de5a9f48b57054b0b8b8301adc1118e2cead87
|
|
7
|
+
data.tar.gz: 87e66da4b78467b289dafc3a280a7f9592af77e39f6878128c3586a0ea7ade24e2589239e8beb35d43c8e52e31ca79626f04d1ee1f31347b55f5ddf41945145a
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20261011
|
|
4
|
+
|
|
5
|
+
1.6.0: The response's uri is the URI it came from, which, where redirects were followed, is where they ended.
|
|
6
|
+
|
|
7
|
+
Nothing recorded where a response had come from: the request objects are made from a path rather than a URI, so Net::HTTP left response.uri nil, and a caller whose request was redirected had no way of knowing where to. A spider resolving the links on a page against the page's address needs it, as does anything noticing that it has been sent to a login page.
|
|
8
|
+
|
|
9
|
+
1. ~ lib/HTTP/REDIRECT.rb: + HTTP.response_uri, which leaves out any credentials embedded in the URI, a response being something which is passed around and printed.
|
|
10
|
+
2. ~ HTTP.request: response.uri is set to response_uri of the URI requested, so that after redirects it is the last of them.
|
|
11
|
+
3. ~ HTTP::TooManyRedirects: the message names the URI of the redirect not followed as well as its Location.
|
|
12
|
+
4. ~ test/HTTP/REDIRECT_test.rb: + the final URI without a redirect, after one, with no_redirect, and without embedded credentials; the URI of the redirect TooManyRedirects carries. All fail against 1.5.0.
|
|
13
|
+
5. ~ README.md: + the response's uri, under Redirections, and in the Limiting redirections example.
|
|
14
|
+
6. ~ HTTP::VERSION: /1.5.0/1.6.0/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 20261011
|
|
18
|
+
|
|
19
|
+
1.5.0: + max_redirects; + HTTP::TooManyRedirects.
|
|
20
|
+
|
|
21
|
+
Redirects were followed without limit, each by a call nested in the last, so a redirect loop ran until SystemStackError, which against a local server came after 2,179 requests. Now no more than max_redirects are followed, 20 unless given, which is where browsers stop, and one more raises HTTP::TooManyRedirects.
|
|
22
|
+
|
|
23
|
+
1. ~ lib/HTTP/REDIRECT.rb: + HTTP::REDIRECT::MAX (20); + HTTP::TooManyRedirects, whose response is the redirect which was not followed.
|
|
24
|
+
2. ~ HTTP.redirect_options: + max_redirects, which the redirect is made with one fewer of.
|
|
25
|
+
3. ~ HTTP.request: + max_redirects option, which defaults to HTTP::REDIRECT::MAX and is not passed through to Net::HTTP; HTTP::TooManyRedirects is raised at a redirect when none are left. max_redirects: 0 raises at the first redirect, where no_redirect: true returns it, and no_redirect wins where both are given.
|
|
26
|
+
4. ~ test/HTTP/REDIRECT_test.rb: + as many redirects followed as allowed, TooManyRedirects for one more with the redirect not followed, 0 raising at the first, no_redirect winning over it, a loop stopped at the default, and the option not passed through to Net::HTTP; max_redirects among the options left as they were. All fail against 1.4.0, which passed max_redirects through to Net::HTTP, where there is no max_redirects= for it.
|
|
27
|
+
5. ~ README.md: + Limiting redirections; + max_redirects among the options which aren't passed through to Net::HTTP.
|
|
28
|
+
6. ~ HTTP::VERSION: /1.4.0/1.5.0/
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## 20261011
|
|
4
32
|
|
|
5
33
|
1.4.0: The method on a redirect is as RFC 9110 has it: a DELETE redirected by a 301 or 302 is now a DELETE, a PUT a PUT, and a HEAD a HEAD.
|
data/README.md
CHANGED
|
@@ -112,6 +112,14 @@ A body repeated keeps its `Content-Type`, form data's included. Where the method
|
|
|
112
112
|
|
|
113
113
|
Redirects are followed with the headers they were given, except that `Authorization` and `Cookie`, and the `username:` and `password:` options, go no further than the origin (scheme, host, and port) they were given to. Where a redirect makes the request a GET, `Content-Type` is left behind, there being no body for it to describe.
|
|
114
114
|
|
|
115
|
+
The response's `uri` is the URI it came from, which, where redirects were followed, is where they ended:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
response = HTTP.get('http://example.com/moved')
|
|
119
|
+
response.uri
|
|
120
|
+
# => #<URI::HTTP http://example.com/new_location>
|
|
121
|
+
```
|
|
122
|
+
|
|
115
123
|
### Preventing redirections
|
|
116
124
|
|
|
117
125
|
```ruby
|
|
@@ -119,6 +127,23 @@ HTTP.get('http://example.com', {}, {}, {no_redirect: true})
|
|
|
119
127
|
# => #<Net::HTTPResponse @code=3xx>
|
|
120
128
|
```
|
|
121
129
|
|
|
130
|
+
### Limiting redirections
|
|
131
|
+
|
|
132
|
+
Up to 20 redirects are followed by default. One more raises `HTTP::TooManyRedirects`, whose `response` is the redirect which wasn't followed:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
HTTP.get('http://example.com', {}, {}, {max_redirects: 5})
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
HTTP.get('http://example.com/loop')
|
|
139
|
+
rescue HTTP::TooManyRedirects => e
|
|
140
|
+
e.response.uri
|
|
141
|
+
e.response['location']
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`max_redirects: 0` raises at the first redirect, whereas `no_redirect: true` returns it.
|
|
146
|
+
|
|
122
147
|
### Retries
|
|
123
148
|
|
|
124
149
|
Retries are disabled by default. Enable them by passing `retries:` in the options hash.
|
|
@@ -216,12 +241,16 @@ HTTP.request_with_body(:propfind, 'http://example.com/path', xml, {'Content-Type
|
|
|
216
241
|
Both take the same `(method, uri, args_or_data, headers, options, &block)` arguments as the named methods, with the method as the first argument, and the named methods delegate to them. For full control over the request object, drop down to `HTTP.request(uri, request_object, headers, options, &block)`.
|
|
217
242
|
|
|
218
243
|
## Allowed values for the options hash
|
|
219
|
-
#### (These pass through to Net::HTTP, except for `no_redirect`, `username`, and `password`.)
|
|
244
|
+
#### (These pass through to Net::HTTP, except for `no_redirect`, `max_redirects`, `username`, and `password`.)
|
|
220
245
|
|
|
221
246
|
```Ruby
|
|
222
247
|
no_redirect
|
|
223
248
|
# Prevents redirection if a 3xx response is encountered.
|
|
224
249
|
|
|
250
|
+
max_redirects
|
|
251
|
+
# The most redirects followed before HTTP::TooManyRedirects is raised.
|
|
252
|
+
# Defaults to 20.
|
|
253
|
+
|
|
225
254
|
username
|
|
226
255
|
# Username for HTTP basic authentication. Takes precedence over any
|
|
227
256
|
# credentials embedded in the URI.
|
data/lib/HTTP/REDIRECT.rb
CHANGED
|
@@ -5,6 +5,16 @@ module HTTP
|
|
|
5
5
|
module REDIRECT
|
|
6
6
|
CODES = %w{301 302 303 307 308}.freeze
|
|
7
7
|
CREDENTIAL_HEADERS = %w{authorization cookie}.freeze
|
|
8
|
+
MAX = 20
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class TooManyRedirects < StandardError
|
|
12
|
+
attr_reader :response
|
|
13
|
+
|
|
14
|
+
def initialize(response)
|
|
15
|
+
@response = response
|
|
16
|
+
super("Too many redirects: #{response.code} from #{response.uri} to #{response['location']}")
|
|
17
|
+
end
|
|
8
18
|
end
|
|
9
19
|
|
|
10
20
|
# A 300 or a 304 is an answer rather than somewhere else to go, and a 3xx without a
|
|
@@ -52,9 +62,20 @@ module HTTP
|
|
|
52
62
|
end
|
|
53
63
|
module_function :body_headers
|
|
54
64
|
|
|
55
|
-
def redirect_options(options, from_uri, to_uri)
|
|
65
|
+
def redirect_options(options, from_uri, to_uri, max_redirects)
|
|
66
|
+
options = options.merge(max_redirects: max_redirects - 1)
|
|
56
67
|
return options if same_origin?(from_uri, to_uri)
|
|
57
68
|
options.reject{|k, v| %i{username password}.include?(k)}
|
|
58
69
|
end
|
|
59
70
|
module_function :redirect_options
|
|
71
|
+
|
|
72
|
+
# Embedded credentials are left out, a response being something which is passed
|
|
73
|
+
# around and printed. URI#userinfo= ignores nil, whereas URI#user= takes the password
|
|
74
|
+
# with it.
|
|
75
|
+
def response_uri(uri)
|
|
76
|
+
uri = uri.dup
|
|
77
|
+
uri.user = nil if uri.user
|
|
78
|
+
uri
|
|
79
|
+
end
|
|
80
|
+
module_function :response_uri
|
|
60
81
|
end
|
data/lib/HTTP/VERSION.rb
CHANGED
data/lib/HTTP/request.rb
CHANGED
|
@@ -19,6 +19,7 @@ module HTTP
|
|
|
19
19
|
given_options = options
|
|
20
20
|
options = options.dup
|
|
21
21
|
no_redirect = options.delete(:no_redirect)
|
|
22
|
+
max_redirects = options.delete(:max_redirects) || REDIRECT::MAX
|
|
22
23
|
username = options.delete(:username)
|
|
23
24
|
password = options.delete(:password)
|
|
24
25
|
config = retry_config(options)
|
|
@@ -40,9 +41,11 @@ module HTTP
|
|
|
40
41
|
http.request(request_object)
|
|
41
42
|
end
|
|
42
43
|
)
|
|
44
|
+
response.uri = response_uri(uri)
|
|
43
45
|
if !no_redirect && redirect?(response)
|
|
46
|
+
raise TooManyRedirects.new(response) if max_redirects < 1
|
|
44
47
|
redirect_uri = uri.merge(response['location'])
|
|
45
|
-
onward_options = redirect_options(given_options, uri, redirect_uri)
|
|
48
|
+
onward_options = redirect_options(given_options, uri, redirect_uri, max_redirects)
|
|
46
49
|
redirect_method = redirect_method(response.code, request_method)
|
|
47
50
|
if redirect_method != request_method
|
|
48
51
|
return request_without_body(redirect_method, redirect_uri, {}, redirect_headers(headers, uri, redirect_uri, drop_content_type: true), onward_options, &block)
|
data/test/HTTP/REDIRECT_test.rb
CHANGED
|
@@ -7,6 +7,29 @@ describe "redirect behaviour" do
|
|
|
7
7
|
let(:same_origin_uri){'http://example.com/elsewhere'}
|
|
8
8
|
let(:other_origin_uri){'http://redirected.com/path'}
|
|
9
9
|
|
|
10
|
+
describe "the final URI" do
|
|
11
|
+
it "is the requested URI when there is no redirect" do
|
|
12
|
+
stub_request(:get, uri).to_return(status: 200)
|
|
13
|
+
_(HTTP.get(uri).uri.to_s).must_equal(uri)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "is the URI redirected to when a redirect is followed" do
|
|
17
|
+
stub_request(:get, uri).to_return(status: 302, headers: {'location' => other_origin_uri})
|
|
18
|
+
stub_request(:get, other_origin_uri).to_return(status: 200)
|
|
19
|
+
_(HTTP.get(uri).uri.to_s).must_equal(other_origin_uri)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "is the URI which redirected when no_redirect is set" do
|
|
23
|
+
stub_request(:get, uri).to_return(status: 302, headers: {'location' => other_origin_uri})
|
|
24
|
+
_(HTTP.get(uri, {}, {}, {no_redirect: true}).uri.to_s).must_equal(uri)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "carries no embedded credentials" do
|
|
28
|
+
stub_request(:get, uri).with(basic_auth: ['alice', 'secret']).to_return(status: 200)
|
|
29
|
+
_(HTTP.get('http://alice:secret@example.com/path').uri.to_s).must_equal(uri)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
10
33
|
describe "headers" do
|
|
11
34
|
%w{301 302 303 307 308}.each do |code|
|
|
12
35
|
it "are carried through a #{code}" do
|
|
@@ -59,9 +82,9 @@ describe "redirect behaviour" do
|
|
|
59
82
|
it "is not altered by a call" do
|
|
60
83
|
stub_request(:get, uri).to_return(status: 302, headers: {'location' => same_origin_uri})
|
|
61
84
|
stub_request(:get, same_origin_uri).to_return(status: 200)
|
|
62
|
-
options = {no_redirect: false, username: 'alice', password: 'secret', retries: 1}
|
|
85
|
+
options = {no_redirect: false, max_redirects: 3, username: 'alice', password: 'secret', retries: 1}
|
|
63
86
|
HTTP.get(uri, {}, {}, options)
|
|
64
|
-
_(options).must_equal({no_redirect: false, username: 'alice', password: 'secret', retries: 1})
|
|
87
|
+
_(options).must_equal({no_redirect: false, max_redirects: 3, username: 'alice', password: 'secret', retries: 1})
|
|
65
88
|
end
|
|
66
89
|
|
|
67
90
|
it "keeps no_redirect for a second call" do
|
|
@@ -215,4 +238,43 @@ describe "redirect behaviour" do
|
|
|
215
238
|
end
|
|
216
239
|
end
|
|
217
240
|
end
|
|
241
|
+
|
|
242
|
+
describe "max_redirects" do
|
|
243
|
+
before do
|
|
244
|
+
stub_request(:get, 'http://example.com/0').to_return(status: 302, headers: {'location' => '/1'})
|
|
245
|
+
stub_request(:get, 'http://example.com/1').to_return(status: 302, headers: {'location' => '/2'})
|
|
246
|
+
stub_request(:get, 'http://example.com/2').to_return(status: 200)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it "follows as many redirects as it is allowed" do
|
|
250
|
+
_(HTTP.get('http://example.com/0', {}, {}, {max_redirects: 2}).success?).must_equal(true)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "raises TooManyRedirects, with the redirect not followed, when there are more" do
|
|
254
|
+
error = _(->{HTTP.get('http://example.com/0', {}, {}, {max_redirects: 1})}).must_raise(HTTP::TooManyRedirects)
|
|
255
|
+
_(error.response['location']).must_equal('/2')
|
|
256
|
+
_(error.response.uri.to_s).must_equal('http://example.com/1')
|
|
257
|
+
assert_not_requested(:get, 'http://example.com/2')
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "raises at the first redirect when it is 0" do
|
|
261
|
+
_(->{HTTP.get('http://example.com/0', {}, {}, {max_redirects: 0})}).must_raise(HTTP::TooManyRedirects)
|
|
262
|
+
assert_not_requested(:get, 'http://example.com/1')
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "gives way to no_redirect" do
|
|
266
|
+
_(HTTP.get('http://example.com/0', {}, {}, {max_redirects: 0, no_redirect: true}).code).must_equal('302')
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "stops a redirect loop at the default" do
|
|
270
|
+
stub_request(:get, uri).to_return(status: 302, headers: {'location' => uri})
|
|
271
|
+
_(->{HTTP.get(uri)}).must_raise(HTTP::TooManyRedirects)
|
|
272
|
+
assert_requested(:get, uri, times: HTTP::REDIRECT::MAX + 1)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it "is not passed through to Net::HTTP" do
|
|
276
|
+
stub_request(:get, uri).to_return(status: 200)
|
|
277
|
+
_(HTTP.get(uri, {}, {}, {max_redirects: 5}).success?).must_equal(true)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
218
280
|
end
|