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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd64e430f4c763d9a11367ecc26c6d419f9b5a0304c8084c41f995187d5b179a
4
- data.tar.gz: bd8d9c977ed8e594a80c97a65d546270cf15dc482b2ee4440e5eeb739c2240b9
3
+ metadata.gz: 2483f99ec13020c1e1c780b84c7dcddc68437687b1176e7fb88ba228adf084af
4
+ data.tar.gz: 710dd8eed5142ac832553595e581bd2597174dd1131fb14a7c51d4e7f17f4992
5
5
  SHA512:
6
- metadata.gz: 86146298fe511634f52a083fce07d96b3524a7faf46611cb262bf3e502c4ad413a233081091936a798649b6d9126061945ef34ef0b4a8e2c12ae8f0f2c3a87df
7
- data.tar.gz: 8d6138d16b159c467dd645e675b7f0753e2693eb0a31c2cdf953731c90a0b9bde5ee3a90cee29ed7be928398a28ad27ab2bfe784cd9eafa66111c2cec4a42d98
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
@@ -2,5 +2,5 @@
2
2
  # HTTP::VERSION
3
3
 
4
4
  module HTTP
5
- VERSION = '1.4.0'
5
+ VERSION = '1.6.0'
6
6
  end
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)
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran