http.rb 1.4.0 → 1.5.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: 72cf60480847b43b60b63378d803acae19a12c127ceb9695ed8b0eca99ca57a4
4
+ data.tar.gz: aed0e179795a1f408823cb5cecca6bb2c877a1e4a90c3a31f21a115a5270a2e6
5
5
  SHA512:
6
- metadata.gz: 86146298fe511634f52a083fce07d96b3524a7faf46611cb262bf3e502c4ad413a233081091936a798649b6d9126061945ef34ef0b4a8e2c12ae8f0f2c3a87df
7
- data.tar.gz: 8d6138d16b159c467dd645e675b7f0753e2693eb0a31c2cdf953731c90a0b9bde5ee3a90cee29ed7be928398a28ad27ab2bfe784cd9eafa66111c2cec4a42d98
6
+ metadata.gz: b8e095a766f24d074324536e89dc07871bc8670785fe14f95470a06e64a4107e0c7c2df44f746c8dbd9c31d7b79336e8548aaf5601e4121db86f5787c1ee66bb
7
+ data.tar.gz: c5637e2490a1605d4112162bad17c12d70e1536af814901c1bdc2572eb730f15e54b0eb8db2db3ff6057d872b9969b7852c5767872b9108ce257a160a9ebf24f
data/CHANGELOG CHANGED
@@ -1,5 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20261011
4
+
5
+ 1.5.0: + max_redirects; + HTTP::TooManyRedirects.
6
+
7
+ 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.
8
+
9
+ 1. ~ lib/HTTP/REDIRECT.rb: + HTTP::REDIRECT::MAX (20); + HTTP::TooManyRedirects, whose response is the redirect which was not followed.
10
+ 2. ~ HTTP.redirect_options: + max_redirects, which the redirect is made with one fewer of.
11
+ 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.
12
+ 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.
13
+ 5. ~ README.md: + Limiting redirections; + max_redirects among the options which aren't passed through to Net::HTTP.
14
+ 6. ~ HTTP::VERSION: /1.4.0/1.5.0/
15
+
16
+
3
17
  ## 20261011
4
18
 
5
19
  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
@@ -119,6 +119,22 @@ HTTP.get('http://example.com', {}, {}, {no_redirect: true})
119
119
  # => #<Net::HTTPResponse @code=3xx>
120
120
  ```
121
121
 
122
+ ### Limiting redirections
123
+
124
+ Up to 20 redirects are followed by default. One more raises `HTTP::TooManyRedirects`, whose `response` is the redirect which wasn't followed:
125
+
126
+ ```ruby
127
+ HTTP.get('http://example.com', {}, {}, {max_redirects: 5})
128
+
129
+ begin
130
+ HTTP.get('http://example.com/loop')
131
+ rescue HTTP::TooManyRedirects => e
132
+ e.response['location']
133
+ end
134
+ ```
135
+
136
+ `max_redirects: 0` raises at the first redirect, whereas `no_redirect: true` returns it.
137
+
122
138
  ### Retries
123
139
 
124
140
  Retries are disabled by default. Enable them by passing `retries:` in the options hash.
@@ -216,12 +232,16 @@ HTTP.request_with_body(:propfind, 'http://example.com/path', xml, {'Content-Type
216
232
  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
233
 
218
234
  ## Allowed values for the options hash
219
- #### (These pass through to Net::HTTP, except for `no_redirect`, `username`, and `password`.)
235
+ #### (These pass through to Net::HTTP, except for `no_redirect`, `max_redirects`, `username`, and `password`.)
220
236
 
221
237
  ```Ruby
222
238
  no_redirect
223
239
  # Prevents redirection if a 3xx response is encountered.
224
240
 
241
+ max_redirects
242
+ # The most redirects followed before HTTP::TooManyRedirects is raised.
243
+ # Defaults to 20.
244
+
225
245
  username
226
246
  # Username for HTTP basic authentication. Takes precedence over any
227
247
  # 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} 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,7 +62,8 @@ 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
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.5.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)
@@ -41,8 +42,9 @@ module HTTP
41
42
  end
42
43
  )
43
44
  if !no_redirect && redirect?(response)
45
+ raise TooManyRedirects.new(response) if max_redirects < 1
44
46
  redirect_uri = uri.merge(response['location'])
45
- onward_options = redirect_options(given_options, uri, redirect_uri)
47
+ onward_options = redirect_options(given_options, uri, redirect_uri, max_redirects)
46
48
  redirect_method = redirect_method(response.code, request_method)
47
49
  if redirect_method != request_method
48
50
  return request_without_body(redirect_method, redirect_uri, {}, redirect_headers(headers, uri, redirect_uri, drop_content_type: true), onward_options, &block)
@@ -59,9 +59,9 @@ describe "redirect behaviour" do
59
59
  it "is not altered by a call" do
60
60
  stub_request(:get, uri).to_return(status: 302, headers: {'location' => same_origin_uri})
61
61
  stub_request(:get, same_origin_uri).to_return(status: 200)
62
- options = {no_redirect: false, username: 'alice', password: 'secret', retries: 1}
62
+ options = {no_redirect: false, max_redirects: 3, username: 'alice', password: 'secret', retries: 1}
63
63
  HTTP.get(uri, {}, {}, options)
64
- _(options).must_equal({no_redirect: false, username: 'alice', password: 'secret', retries: 1})
64
+ _(options).must_equal({no_redirect: false, max_redirects: 3, username: 'alice', password: 'secret', retries: 1})
65
65
  end
66
66
 
67
67
  it "keeps no_redirect for a second call" do
@@ -215,4 +215,42 @@ describe "redirect behaviour" do
215
215
  end
216
216
  end
217
217
  end
218
+
219
+ describe "max_redirects" do
220
+ before do
221
+ stub_request(:get, 'http://example.com/0').to_return(status: 302, headers: {'location' => '/1'})
222
+ stub_request(:get, 'http://example.com/1').to_return(status: 302, headers: {'location' => '/2'})
223
+ stub_request(:get, 'http://example.com/2').to_return(status: 200)
224
+ end
225
+
226
+ it "follows as many redirects as it is allowed" do
227
+ _(HTTP.get('http://example.com/0', {}, {}, {max_redirects: 2}).success?).must_equal(true)
228
+ end
229
+
230
+ it "raises TooManyRedirects, with the redirect not followed, when there are more" do
231
+ error = _(->{HTTP.get('http://example.com/0', {}, {}, {max_redirects: 1})}).must_raise(HTTP::TooManyRedirects)
232
+ _(error.response['location']).must_equal('/2')
233
+ assert_not_requested(:get, 'http://example.com/2')
234
+ end
235
+
236
+ it "raises at the first redirect when it is 0" do
237
+ _(->{HTTP.get('http://example.com/0', {}, {}, {max_redirects: 0})}).must_raise(HTTP::TooManyRedirects)
238
+ assert_not_requested(:get, 'http://example.com/1')
239
+ end
240
+
241
+ it "gives way to no_redirect" do
242
+ _(HTTP.get('http://example.com/0', {}, {}, {max_redirects: 0, no_redirect: true}).code).must_equal('302')
243
+ end
244
+
245
+ it "stops a redirect loop at the default" do
246
+ stub_request(:get, uri).to_return(status: 302, headers: {'location' => uri})
247
+ _(->{HTTP.get(uri)}).must_raise(HTTP::TooManyRedirects)
248
+ assert_requested(:get, uri, times: HTTP::REDIRECT::MAX + 1)
249
+ end
250
+
251
+ it "is not passed through to Net::HTTP" do
252
+ stub_request(:get, uri).to_return(status: 200)
253
+ _(HTTP.get(uri, {}, {}, {max_redirects: 5}).success?).must_equal(true)
254
+ end
255
+ end
218
256
  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.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran