http 5.2.0 → 5.3.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/.rubocop/rspec.yml +9 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +17 -1
- data/Gemfile +1 -0
- data/http.gemspec +7 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/connection.rb +4 -2
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +3 -2
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/redirector_spec.rb +6 -5
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +13 -0
- data/spec/support/dummy_server.rb +2 -1
- metadata +21 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da85aeba1d2d3dce86f59a678ade3f74df35514397cb7488f9aaab9ea5a57220
|
4
|
+
data.tar.gz: 28e5143890592f55f51fa65874f787f3c3e9eac5f9f6103c589082414eff378e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b14bb800aefa920d2511b962967253fc034847bb07f8d181bb9cec6dba136ca0c51655fd0da716986897be5ef83e6023705578016db7bbd12bbd1f2ed4bbeb4
|
7
|
+
data.tar.gz: dc996c20d358b382fbb2b1d93af7a706f858167392c8db3d8bf11e114419c0c7ed0a61350753f9c13422ca48b469af0b359f80694fe423066a02439066bfdd07
|
data/.rubocop/rspec.yml
ADDED
data/.rubocop_todo.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100`
|
3
|
-
# on
|
3
|
+
# on 2025-06-09 02:44:43 UTC using RuboCop version 1.30.1.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
@@ -14,7 +14,7 @@ Gemspec/DeprecatedAttributeAssignment:
|
|
14
14
|
Exclude:
|
15
15
|
- 'http.gemspec'
|
16
16
|
|
17
|
-
# Offense count:
|
17
|
+
# Offense count: 55
|
18
18
|
# This cop supports safe autocorrection (--autocorrect).
|
19
19
|
# Configuration parameters: EnforcedStyle.
|
20
20
|
# SupportedStyles: leading, trailing
|
@@ -30,7 +30,14 @@ Layout/DotPosition:
|
|
30
30
|
- 'spec/lib/http_spec.rb'
|
31
31
|
- 'spec/support/http_handling_shared.rb'
|
32
32
|
|
33
|
-
# Offense count:
|
33
|
+
# Offense count: 2
|
34
|
+
# This cop supports safe autocorrection (--autocorrect).
|
35
|
+
# Configuration parameters: IndentationWidth.
|
36
|
+
# SupportedStyles: special_inside_parentheses, consistent, align_braces
|
37
|
+
Layout/FirstHashElementIndentation:
|
38
|
+
EnforcedStyle: consistent
|
39
|
+
|
40
|
+
# Offense count: 206
|
34
41
|
# This cop supports safe autocorrection (--autocorrect).
|
35
42
|
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
36
43
|
# SupportedStyles: space, no_space, compact
|
@@ -62,6 +69,18 @@ Lint/MissingSuper:
|
|
62
69
|
- 'lib/http/features/logging.rb'
|
63
70
|
- 'lib/http/features/normalize_uri.rb'
|
64
71
|
|
72
|
+
# Offense count: 1
|
73
|
+
# This cop supports safe autocorrection (--autocorrect).
|
74
|
+
Lint/RedundantCopDisableDirective:
|
75
|
+
Exclude:
|
76
|
+
- 'spec/lib/http/retriable/performer_spec.rb'
|
77
|
+
|
78
|
+
# Offense count: 6
|
79
|
+
# Configuration parameters: AllowComments, AllowNil.
|
80
|
+
Lint/SuppressedException:
|
81
|
+
Exclude:
|
82
|
+
- 'spec/lib/http/retriable/performer_spec.rb'
|
83
|
+
|
65
84
|
# Offense count: 8
|
66
85
|
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes, Max.
|
67
86
|
Metrics/AbcSize:
|
@@ -74,34 +93,6 @@ Metrics/AbcSize:
|
|
74
93
|
- 'lib/http/request.rb'
|
75
94
|
- 'lib/http/response.rb'
|
76
95
|
|
77
|
-
# Offense count: 70
|
78
|
-
# Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
|
79
|
-
# IgnoredMethods: refine
|
80
|
-
Metrics/BlockLength:
|
81
|
-
Exclude:
|
82
|
-
- '**/*.gemspec'
|
83
|
-
- 'spec/lib/http/client_spec.rb'
|
84
|
-
- 'spec/lib/http/connection_spec.rb'
|
85
|
-
- 'spec/lib/http/content_type_spec.rb'
|
86
|
-
- 'spec/lib/http/features/auto_deflate_spec.rb'
|
87
|
-
- 'spec/lib/http/features/auto_inflate_spec.rb'
|
88
|
-
- 'spec/lib/http/features/instrumentation_spec.rb'
|
89
|
-
- 'spec/lib/http/features/logging_spec.rb'
|
90
|
-
- 'spec/lib/http/headers/mixin_spec.rb'
|
91
|
-
- 'spec/lib/http/headers_spec.rb'
|
92
|
-
- 'spec/lib/http/options/merge_spec.rb'
|
93
|
-
- 'spec/lib/http/redirector_spec.rb'
|
94
|
-
- 'spec/lib/http/request/body_spec.rb'
|
95
|
-
- 'spec/lib/http/request/writer_spec.rb'
|
96
|
-
- 'spec/lib/http/request_spec.rb'
|
97
|
-
- 'spec/lib/http/response/body_spec.rb'
|
98
|
-
- 'spec/lib/http/response/parser_spec.rb'
|
99
|
-
- 'spec/lib/http/response/status_spec.rb'
|
100
|
-
- 'spec/lib/http/response_spec.rb'
|
101
|
-
- 'spec/lib/http/uri_spec.rb'
|
102
|
-
- 'spec/lib/http_spec.rb'
|
103
|
-
- 'spec/support/http_handling_shared.rb'
|
104
|
-
|
105
96
|
# Offense count: 4
|
106
97
|
# Configuration parameters: CountComments, Max, CountAsOne.
|
107
98
|
Metrics/ClassLength:
|
@@ -118,7 +109,7 @@ Metrics/CyclomaticComplexity:
|
|
118
109
|
- 'lib/http/chainable.rb'
|
119
110
|
- 'lib/http/client.rb'
|
120
111
|
|
121
|
-
# Offense count:
|
112
|
+
# Offense count: 19
|
122
113
|
# Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
|
123
114
|
Metrics/MethodLength:
|
124
115
|
Exclude:
|
@@ -133,6 +124,7 @@ Metrics/MethodLength:
|
|
133
124
|
- 'lib/http/request.rb'
|
134
125
|
- 'lib/http/response.rb'
|
135
126
|
- 'lib/http/response/body.rb'
|
127
|
+
- 'lib/http/retriable/performer.rb'
|
136
128
|
- 'lib/http/timeout/global.rb'
|
137
129
|
|
138
130
|
# Offense count: 1
|
@@ -173,6 +165,27 @@ Style/Encoding:
|
|
173
165
|
- 'spec/lib/http_spec.rb'
|
174
166
|
- 'spec/support/dummy_server/servlet.rb'
|
175
167
|
|
168
|
+
# Offense count: 71
|
169
|
+
# This cop supports safe autocorrection (--autocorrect).
|
170
|
+
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
|
171
|
+
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
|
172
|
+
# SupportedShorthandSyntax: always, never, either
|
173
|
+
Style/HashSyntax:
|
174
|
+
Exclude:
|
175
|
+
- 'spec/lib/http/features/raise_error_spec.rb'
|
176
|
+
- 'spec/lib/http/retriable/delay_calculator_spec.rb'
|
177
|
+
- 'spec/lib/http/retriable/performer_spec.rb'
|
178
|
+
- 'spec/lib/http_spec.rb'
|
179
|
+
|
180
|
+
# Offense count: 4
|
181
|
+
# This cop supports unsafe autocorrection (--autocorrect-all).
|
182
|
+
# Configuration parameters: EnforcedStyle.
|
183
|
+
# SupportedStyles: literals, strict
|
184
|
+
Style/MutableConstant:
|
185
|
+
Exclude:
|
186
|
+
- 'lib/http/headers/normalizer.rb'
|
187
|
+
- 'lib/http/retriable/delay_calculator.rb'
|
188
|
+
|
176
189
|
# Offense count: 17
|
177
190
|
# Configuration parameters: SuspiciousParamNames, Allowlist.
|
178
191
|
# SuspiciousParamNames: options, opts, args, params, parameters
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [5.3.0] - 2025-06-09
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- (backported) Add .retriable feature to Http
|
15
|
+
- (backported) Add more specific ConnectionError classes
|
16
|
+
- (backported) New feature: RaiseError
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
|
20
|
+
- (backported) Drop depenency on base64
|
21
|
+
- (backported) Cache header normalization to reduce object allocation
|
22
|
+
- (backported) Use native llhttp on MRI
|
23
|
+
|
24
|
+
|
10
25
|
## [5.2.0] - 2024-02-05
|
11
26
|
|
12
27
|
### Added
|
@@ -37,5 +52,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
37
52
|
- Prevent CRLF injection due to broken URL normalizer
|
38
53
|
([#765](https://github.com/httprb/http/pull/765))
|
39
54
|
|
40
|
-
[unreleased]: https://github.com/httprb/http/compare/v5.
|
55
|
+
[unreleased]: https://github.com/httprb/http/compare/v5.3.0...5-x-stable
|
56
|
+
[5.3.0]: https://github.com/httprb/http/compare/v5.2.0...v5.3.0
|
41
57
|
[5.2.0]: https://github.com/httprb/http/compare/v5.1.1...v5.2.0
|
data/Gemfile
CHANGED
data/http.gemspec
CHANGED
@@ -28,10 +28,15 @@ Gem::Specification.new do |gem|
|
|
28
28
|
gem.required_ruby_version = ">= 2.6"
|
29
29
|
|
30
30
|
gem.add_runtime_dependency "addressable", "~> 2.8"
|
31
|
-
gem.add_runtime_dependency "base64", "~> 0.1"
|
32
31
|
gem.add_runtime_dependency "http-cookie", "~> 1.0"
|
33
32
|
gem.add_runtime_dependency "http-form_data", "~> 2.2"
|
34
|
-
|
33
|
+
|
34
|
+
# Use native llhttp for MRI (more performant) and llhttp-ffi for other interpreters (better compatibility)
|
35
|
+
if RUBY_ENGINE == "ruby"
|
36
|
+
gem.add_runtime_dependency "llhttp", "~> 0.5.0"
|
37
|
+
else
|
38
|
+
gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
|
39
|
+
end
|
35
40
|
|
36
41
|
gem.add_development_dependency "bundler", "~> 2.0"
|
37
42
|
|
data/lib/http/base64.rb
ADDED
data/lib/http/chainable.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "base64"
|
4
|
-
|
3
|
+
require "http/base64"
|
5
4
|
require "http/headers"
|
6
5
|
|
7
6
|
module HTTP
|
8
7
|
module Chainable
|
8
|
+
include HTTP::Base64
|
9
|
+
|
9
10
|
# Request a get sans response body
|
10
11
|
# @param uri
|
11
12
|
# @option options [Hash]
|
@@ -215,7 +216,7 @@ module HTTP
|
|
215
216
|
pass = opts.fetch(:pass)
|
216
217
|
creds = "#{user}:#{pass}"
|
217
218
|
|
218
|
-
auth("Basic #{
|
219
|
+
auth("Basic #{encode64(creds)}")
|
219
220
|
end
|
220
221
|
|
221
222
|
# Get options for HTTP
|
@@ -242,11 +243,34 @@ module HTTP
|
|
242
243
|
# * instrumentation
|
243
244
|
# * logging
|
244
245
|
# * normalize_uri
|
246
|
+
# * raise_error
|
245
247
|
# @param features
|
246
248
|
def use(*features)
|
247
249
|
branch default_options.with_features(features)
|
248
250
|
end
|
249
251
|
|
252
|
+
# Returns retriable client instance, which retries requests if they failed
|
253
|
+
# due to some socket errors or response status is `5xx`.
|
254
|
+
#
|
255
|
+
# @example Usage
|
256
|
+
#
|
257
|
+
# # Retry max 5 times with randomly growing delay between retries
|
258
|
+
# HTTP.retriable.get(url)
|
259
|
+
#
|
260
|
+
# # Retry max 3 times with randomly growing delay between retries
|
261
|
+
# HTTP.retriable(times: 3).get(url)
|
262
|
+
#
|
263
|
+
# # Retry max 3 times with 1 sec delay between retries
|
264
|
+
# HTTP.retriable(times: 3, delay: proc { 1 }).get(url)
|
265
|
+
#
|
266
|
+
# # Retry max 3 times with geometrically progressed delay between retries
|
267
|
+
# HTTP.retriable(times: 3, delay: proc { |i| 1 + i*i }).get(url)
|
268
|
+
#
|
269
|
+
# @param (see Performer#initialize)
|
270
|
+
def retriable(**options)
|
271
|
+
Retriable::Client.new(Retriable::Performer.new(options), default_options)
|
272
|
+
end
|
273
|
+
|
250
274
|
private
|
251
275
|
|
252
276
|
# :nodoc:
|
data/lib/http/connection.rb
CHANGED
@@ -105,10 +105,11 @@ module HTTP
|
|
105
105
|
|
106
106
|
# Reads data from socket up until headers are loaded
|
107
107
|
# @return [void]
|
108
|
+
# @raise [ResponseHeaderError] when unable to read response headers
|
108
109
|
def read_headers!
|
109
110
|
until @parser.headers?
|
110
111
|
result = read_more(BUFFER_SIZE)
|
111
|
-
raise
|
112
|
+
raise ResponseHeaderError, "couldn't read response headers" if result == :eof
|
112
113
|
end
|
113
114
|
|
114
115
|
set_keep_alive
|
@@ -217,6 +218,7 @@ module HTTP
|
|
217
218
|
|
218
219
|
# Feeds some more data into parser
|
219
220
|
# @return [void]
|
221
|
+
# @raise [SocketReadError] when unable to read from socket
|
220
222
|
def read_more(size)
|
221
223
|
return if @parser.finished?
|
222
224
|
|
@@ -228,7 +230,7 @@ module HTTP
|
|
228
230
|
@parser << value
|
229
231
|
end
|
230
232
|
rescue IOError, SocketError, SystemCallError => e
|
231
|
-
raise
|
233
|
+
raise SocketReadError, "error reading from socket: #{e}", e.backtrace
|
232
234
|
end
|
233
235
|
end
|
234
236
|
end
|
data/lib/http/errors.rb
CHANGED
@@ -7,6 +7,11 @@ module HTTP
|
|
7
7
|
# Generic Connection error
|
8
8
|
class ConnectionError < Error; end
|
9
9
|
|
10
|
+
# Types of Connection errors
|
11
|
+
class ResponseHeaderError < ConnectionError; end
|
12
|
+
class SocketReadError < ConnectionError; end
|
13
|
+
class SocketWriteError < ConnectionError; end
|
14
|
+
|
10
15
|
# Generic Request error
|
11
16
|
class RequestError < Error; end
|
12
17
|
|
@@ -16,6 +21,17 @@ module HTTP
|
|
16
21
|
# Requested to do something when we're in the wrong state
|
17
22
|
class StateError < ResponseError; end
|
18
23
|
|
24
|
+
# When status code indicates an error
|
25
|
+
class StatusError < ResponseError
|
26
|
+
attr_reader :response
|
27
|
+
|
28
|
+
def initialize(response)
|
29
|
+
@response = response
|
30
|
+
|
31
|
+
super("Unexpected status code #{response.code}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
19
35
|
# Generic Timeout error
|
20
36
|
class TimeoutError < Error; end
|
21
37
|
|
data/lib/http/feature.rb
CHANGED
@@ -20,6 +20,7 @@ end
|
|
20
20
|
|
21
21
|
require "http/features/auto_inflate"
|
22
22
|
require "http/features/auto_deflate"
|
23
|
-
require "http/features/logging"
|
24
23
|
require "http/features/instrumentation"
|
24
|
+
require "http/features/logging"
|
25
25
|
require "http/features/normalize_uri"
|
26
|
+
require "http/features/raise_error"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Features
|
5
|
+
class RaiseError < Feature
|
6
|
+
def initialize(ignore: [])
|
7
|
+
super()
|
8
|
+
|
9
|
+
@ignore = ignore
|
10
|
+
end
|
11
|
+
|
12
|
+
def wrap_response(response)
|
13
|
+
return response if response.code < 400
|
14
|
+
return response if @ignore.include?(response.code)
|
15
|
+
|
16
|
+
raise HTTP::StatusError, response
|
17
|
+
end
|
18
|
+
|
19
|
+
HTTP::Options.register_feature(:raise_error, self)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
class Headers
|
5
|
+
class Normalizer
|
6
|
+
# Matches HTTP header names when in "Canonical-Http-Format"
|
7
|
+
CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/
|
8
|
+
|
9
|
+
# Matches valid header field name according to RFC.
|
10
|
+
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
11
|
+
COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
|
12
|
+
|
13
|
+
NAME_PARTS_SEPARATOR_RE = /[\-_]/
|
14
|
+
|
15
|
+
# @private
|
16
|
+
# Normalized header names cache
|
17
|
+
class Cache
|
18
|
+
MAX_SIZE = 200
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@store = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(key)
|
25
|
+
@store[key]
|
26
|
+
end
|
27
|
+
alias [] get
|
28
|
+
|
29
|
+
def set(key, value)
|
30
|
+
# Maintain cache size
|
31
|
+
@store.delete(@store.each_key.first) while MAX_SIZE <= @store.size
|
32
|
+
|
33
|
+
@store[key] = value
|
34
|
+
end
|
35
|
+
alias []= set
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@cache = Cache.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# Transforms `name` to canonical HTTP header capitalization
|
43
|
+
def call(name)
|
44
|
+
name = -name.to_s
|
45
|
+
value = (@cache[name] ||= -normalize_header(name))
|
46
|
+
|
47
|
+
value.dup
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Transforms `name` to canonical HTTP header capitalization
|
53
|
+
#
|
54
|
+
# @param [String] name
|
55
|
+
# @raise [HeaderError] if normalized name does not
|
56
|
+
# match {COMPLIANT_NAME_RE}
|
57
|
+
# @return [String] canonical HTTP header name
|
58
|
+
def normalize_header(name)
|
59
|
+
return name if CANONICAL_NAME_RE.match?(name)
|
60
|
+
|
61
|
+
normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-")
|
62
|
+
|
63
|
+
return normalized if COMPLIANT_NAME_RE.match?(normalized)
|
64
|
+
|
65
|
+
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/http/headers.rb
CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
|
|
4
4
|
|
5
5
|
require "http/errors"
|
6
6
|
require "http/headers/mixin"
|
7
|
+
require "http/headers/normalizer"
|
7
8
|
require "http/headers/known"
|
8
9
|
|
9
10
|
module HTTP
|
@@ -12,12 +13,31 @@ module HTTP
|
|
12
13
|
extend Forwardable
|
13
14
|
include Enumerable
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
class << self
|
17
|
+
# Coerces given `object` into Headers.
|
18
|
+
#
|
19
|
+
# @raise [Error] if object can't be coerced
|
20
|
+
# @param [#to_hash, #to_h, #to_a] object
|
21
|
+
# @return [Headers]
|
22
|
+
def coerce(object)
|
23
|
+
unless object.is_a? self
|
24
|
+
object = case
|
25
|
+
when object.respond_to?(:to_hash) then object.to_hash
|
26
|
+
when object.respond_to?(:to_h) then object.to_h
|
27
|
+
when object.respond_to?(:to_a) then object.to_a
|
28
|
+
else raise Error, "Can't coerce #{object.inspect} to Headers"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
headers = new
|
32
|
+
object.each { |k, v| headers.add k, v }
|
33
|
+
headers
|
34
|
+
end
|
35
|
+
alias [] coerce
|
17
36
|
|
18
|
-
|
19
|
-
|
20
|
-
|
37
|
+
def normalizer
|
38
|
+
@normalizer ||= Headers::Normalizer.new
|
39
|
+
end
|
40
|
+
end
|
21
41
|
|
22
42
|
# Class constructor.
|
23
43
|
def initialize
|
@@ -194,45 +214,11 @@ module HTTP
|
|
194
214
|
dup.tap { |dupped| dupped.merge! other }
|
195
215
|
end
|
196
216
|
|
197
|
-
class << self
|
198
|
-
# Coerces given `object` into Headers.
|
199
|
-
#
|
200
|
-
# @raise [Error] if object can't be coerced
|
201
|
-
# @param [#to_hash, #to_h, #to_a] object
|
202
|
-
# @return [Headers]
|
203
|
-
def coerce(object)
|
204
|
-
unless object.is_a? self
|
205
|
-
object = case
|
206
|
-
when object.respond_to?(:to_hash) then object.to_hash
|
207
|
-
when object.respond_to?(:to_h) then object.to_h
|
208
|
-
when object.respond_to?(:to_a) then object.to_a
|
209
|
-
else raise Error, "Can't coerce #{object.inspect} to Headers"
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
headers = new
|
214
|
-
object.each { |k, v| headers.add k, v }
|
215
|
-
headers
|
216
|
-
end
|
217
|
-
alias [] coerce
|
218
|
-
end
|
219
|
-
|
220
217
|
private
|
221
218
|
|
222
219
|
# Transforms `name` to canonical HTTP header capitalization
|
223
|
-
#
|
224
|
-
# @param [String] name
|
225
|
-
# @raise [HeaderError] if normalized name does not
|
226
|
-
# match {HEADER_NAME_RE}
|
227
|
-
# @return [String] canonical HTTP header name
|
228
220
|
def normalize_header(name)
|
229
|
-
|
230
|
-
|
231
|
-
normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
|
232
|
-
|
233
|
-
return normalized if normalized =~ COMPLIANT_NAME_RE
|
234
|
-
|
235
|
-
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
|
221
|
+
self.class.normalizer.call(name)
|
236
222
|
end
|
237
223
|
|
238
224
|
# Ensures there is no new line character in the header value
|
data/lib/http/request/writer.rb
CHANGED
@@ -108,6 +108,7 @@ module HTTP
|
|
108
108
|
|
109
109
|
private
|
110
110
|
|
111
|
+
# @raise [SocketWriteError] when unable to write to socket
|
111
112
|
def write(data)
|
112
113
|
until data.empty?
|
113
114
|
length = @socket.write(data)
|
@@ -118,7 +119,7 @@ module HTTP
|
|
118
119
|
rescue Errno::EPIPE
|
119
120
|
raise
|
120
121
|
rescue IOError, SocketError, SystemCallError => e
|
121
|
-
raise
|
122
|
+
raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
|
122
123
|
end
|
123
124
|
end
|
124
125
|
end
|
data/lib/http/request.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "forwardable"
|
4
|
-
require "base64"
|
5
4
|
require "time"
|
6
5
|
|
6
|
+
require "http/base64"
|
7
7
|
require "http/errors"
|
8
8
|
require "http/headers"
|
9
9
|
require "http/request/body"
|
@@ -15,6 +15,7 @@ module HTTP
|
|
15
15
|
class Request
|
16
16
|
extend Forwardable
|
17
17
|
|
18
|
+
include HTTP::Base64
|
18
19
|
include HTTP::Headers::Mixin
|
19
20
|
|
20
21
|
# The method given was not understood
|
@@ -159,7 +160,7 @@ module HTTP
|
|
159
160
|
end
|
160
161
|
|
161
162
|
def proxy_authorization_header
|
162
|
-
digest =
|
163
|
+
digest = encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
|
163
164
|
"Basic #{digest}"
|
164
165
|
end
|
165
166
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http/retriable/performer"
|
4
|
+
|
5
|
+
module HTTP
|
6
|
+
module Retriable
|
7
|
+
# Retriable version of HTTP::Client.
|
8
|
+
#
|
9
|
+
# @see http://www.rubydoc.info/gems/http/HTTP/Client
|
10
|
+
class Client < HTTP::Client
|
11
|
+
# @param [Performer] performer
|
12
|
+
# @param [HTTP::Options, Hash] options
|
13
|
+
def initialize(performer, options)
|
14
|
+
@performer = performer
|
15
|
+
super(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Overriden version of `HTTP::Client#make_request`.
|
19
|
+
#
|
20
|
+
# Monitors request/response phase with performer.
|
21
|
+
#
|
22
|
+
# @see http://www.rubydoc.info/gems/http/HTTP/Client:perform
|
23
|
+
def perform(req, options)
|
24
|
+
@performer.perform(self, req) { super(req, options) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Overriden version of `HTTP::Chainable#branch`.
|
30
|
+
#
|
31
|
+
# @return [HTTP::Retriable::Client]
|
32
|
+
def branch(options)
|
33
|
+
Retriable::Client.new(@performer, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Retriable
|
5
|
+
# @api private
|
6
|
+
class DelayCalculator
|
7
|
+
def initialize(opts)
|
8
|
+
@max_delay = opts.fetch(:max_delay, Float::MAX).to_f
|
9
|
+
if (delay = opts[:delay]).respond_to?(:call)
|
10
|
+
@delay_proc = opts.fetch(:delay)
|
11
|
+
else
|
12
|
+
@delay = delay
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(iteration, response)
|
17
|
+
delay = if response && (retry_header = response.headers["Retry-After"])
|
18
|
+
delay_from_retry_header(retry_header)
|
19
|
+
else
|
20
|
+
calculate_delay_from_iteration(iteration)
|
21
|
+
end
|
22
|
+
|
23
|
+
ensure_dealy_in_bounds(delay)
|
24
|
+
end
|
25
|
+
|
26
|
+
RFC2822_DATE_REGEX = /^
|
27
|
+
(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
|
28
|
+
(?:0[1-9]|[1-2]?[0-9]|3[01])\s+
|
29
|
+
(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
|
30
|
+
(?:19[0-9]{2}|[2-9][0-9]{3})\s+
|
31
|
+
(?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:60|[0-5][0-9])\s+
|
32
|
+
GMT
|
33
|
+
$/x
|
34
|
+
|
35
|
+
# Spec for Retry-After header
|
36
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
37
|
+
def delay_from_retry_header(value)
|
38
|
+
value = value.to_s.strip
|
39
|
+
|
40
|
+
case value
|
41
|
+
when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
|
42
|
+
when /^\d+$/ then value.to_i
|
43
|
+
else 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def calculate_delay_from_iteration(iteration)
|
48
|
+
if @delay_proc
|
49
|
+
@delay_proc.call(iteration)
|
50
|
+
elsif @delay
|
51
|
+
@delay
|
52
|
+
else
|
53
|
+
delay = (2**(iteration - 1)) - 1
|
54
|
+
delay_noise = rand
|
55
|
+
delay + delay_noise
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def ensure_dealy_in_bounds(delay)
|
60
|
+
delay.clamp(0, @max_delay)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|