http 3.1.0 → 5.3.1
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 +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- 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/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -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/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- 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/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -36
data/http.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
lib = File.expand_path("
|
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
require "http/version"
|
|
6
6
|
|
|
@@ -25,12 +25,21 @@ Gem::Specification.new do |gem|
|
|
|
25
25
|
gem.require_paths = ["lib"]
|
|
26
26
|
gem.version = HTTP::VERSION
|
|
27
27
|
|
|
28
|
-
gem.required_ruby_version = ">= 2.
|
|
28
|
+
gem.required_ruby_version = ">= 2.6"
|
|
29
29
|
|
|
30
|
-
gem.add_runtime_dependency "
|
|
31
|
-
gem.add_runtime_dependency "http-form_data", "~> 2.0"
|
|
30
|
+
gem.add_runtime_dependency "addressable", "~> 2.8"
|
|
32
31
|
gem.add_runtime_dependency "http-cookie", "~> 1.0"
|
|
33
|
-
gem.add_runtime_dependency "
|
|
32
|
+
gem.add_runtime_dependency "http-form_data", "~> 2.2"
|
|
34
33
|
|
|
35
|
-
gem.
|
|
34
|
+
gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
|
|
35
|
+
|
|
36
|
+
gem.add_development_dependency "bundler", "~> 2.0"
|
|
37
|
+
|
|
38
|
+
gem.metadata = {
|
|
39
|
+
"source_code_uri" => "https://github.com/httprb/http",
|
|
40
|
+
"wiki_uri" => "https://github.com/httprb/http/wiki",
|
|
41
|
+
"bug_tracker_uri" => "https://github.com/httprb/http/issues",
|
|
42
|
+
"changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGELOG.md",
|
|
43
|
+
"rubygems_mfa_required" => "true"
|
|
44
|
+
}
|
|
36
45
|
end
|
data/lib/http/base64.rb
ADDED
data/lib/http/chainable.rb
CHANGED
|
@@ -1,111 +1,113 @@
|
|
|
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]
|
|
12
|
-
def head(uri, options = {})
|
|
13
|
+
def head(uri, options = {})
|
|
13
14
|
request :head, uri, options
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
# Get a resource
|
|
17
18
|
# @param uri
|
|
18
19
|
# @option options [Hash]
|
|
19
|
-
def get(uri, options = {})
|
|
20
|
+
def get(uri, options = {})
|
|
20
21
|
request :get, uri, options
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Post to a resource
|
|
24
25
|
# @param uri
|
|
25
26
|
# @option options [Hash]
|
|
26
|
-
def post(uri, options = {})
|
|
27
|
+
def post(uri, options = {})
|
|
27
28
|
request :post, uri, options
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Put to a resource
|
|
31
32
|
# @param uri
|
|
32
33
|
# @option options [Hash]
|
|
33
|
-
def put(uri, options = {})
|
|
34
|
+
def put(uri, options = {})
|
|
34
35
|
request :put, uri, options
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# Delete a resource
|
|
38
39
|
# @param uri
|
|
39
40
|
# @option options [Hash]
|
|
40
|
-
def delete(uri, options = {})
|
|
41
|
+
def delete(uri, options = {})
|
|
41
42
|
request :delete, uri, options
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Echo the request back to the client
|
|
45
46
|
# @param uri
|
|
46
47
|
# @option options [Hash]
|
|
47
|
-
def trace(uri, options = {})
|
|
48
|
+
def trace(uri, options = {})
|
|
48
49
|
request :trace, uri, options
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# Return the methods supported on the given URI
|
|
52
53
|
# @param uri
|
|
53
54
|
# @option options [Hash]
|
|
54
|
-
def options(uri, options = {})
|
|
55
|
+
def options(uri, options = {})
|
|
55
56
|
request :options, uri, options
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# Convert to a transparent TCP/IP tunnel
|
|
59
60
|
# @param uri
|
|
60
61
|
# @option options [Hash]
|
|
61
|
-
def connect(uri, options = {})
|
|
62
|
+
def connect(uri, options = {})
|
|
62
63
|
request :connect, uri, options
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
# Apply partial modifications to a resource
|
|
66
67
|
# @param uri
|
|
67
68
|
# @option options [Hash]
|
|
68
|
-
def patch(uri, options = {})
|
|
69
|
+
def patch(uri, options = {})
|
|
69
70
|
request :patch, uri, options
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# Make an HTTP request with the given verb
|
|
73
|
-
# @param
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
# @param (see Client#request)
|
|
75
|
+
def request(*args)
|
|
76
|
+
branch(default_options).request(*args)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Prepare an HTTP request with the given verb
|
|
80
|
+
# @param (see Client#build_request)
|
|
81
|
+
def build_request(*args)
|
|
82
|
+
branch(default_options).build_request(*args)
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
# @overload timeout(options = {})
|
|
80
|
-
#
|
|
81
|
-
# @overload timeout(klass, options = {})
|
|
82
|
-
# Adds a timeout to the request.
|
|
83
|
-
# @param [#to_sym] klass
|
|
84
|
-
# either :null, :global, or :per_operation
|
|
86
|
+
# Adds per operation timeouts to the request
|
|
85
87
|
# @param [Hash] options
|
|
86
88
|
# @option options [Float] :read Read timeout
|
|
87
89
|
# @option options [Float] :write Write timeout
|
|
88
90
|
# @option options [Float] :connect Connect timeout
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
%i[read write connect].each do |k|
|
|
91
|
+
# @overload timeout(global_timeout)
|
|
92
|
+
# Adds a global timeout to the full request
|
|
93
|
+
# @param [Numeric] global_timeout
|
|
94
|
+
def timeout(options)
|
|
95
|
+
klass, options = case options
|
|
96
|
+
when Numeric then [HTTP::Timeout::Global, {:global => options}]
|
|
97
|
+
when Hash then [HTTP::Timeout::PerOperation, options.dup]
|
|
98
|
+
when :null then [HTTP::Timeout::Null, {}]
|
|
99
|
+
else raise ArgumentError, "Use `.timeout(global_timeout_in_seconds)` or `.timeout(connect: x, write: y, read: z)`."
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
%i[global read write connect].each do |k|
|
|
103
104
|
next unless options.key? k
|
|
105
|
+
|
|
104
106
|
options["#{k}_timeout".to_sym] = options.delete k
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
branch default_options.merge(
|
|
108
|
-
:timeout_class
|
|
110
|
+
:timeout_class => klass,
|
|
109
111
|
:timeout_options => options
|
|
110
112
|
)
|
|
111
113
|
end
|
|
@@ -144,9 +146,10 @@ module HTTP
|
|
|
144
146
|
options = {:keep_alive_timeout => timeout}
|
|
145
147
|
p_client = branch default_options.merge(options).with_persistent host
|
|
146
148
|
return p_client unless block_given?
|
|
149
|
+
|
|
147
150
|
yield p_client
|
|
148
151
|
ensure
|
|
149
|
-
p_client
|
|
152
|
+
p_client&.close
|
|
150
153
|
end
|
|
151
154
|
|
|
152
155
|
# Make a request through an HTTP proxy
|
|
@@ -168,10 +171,10 @@ module HTTP
|
|
|
168
171
|
alias through via
|
|
169
172
|
|
|
170
173
|
# Make client follow redirects.
|
|
171
|
-
# @param
|
|
174
|
+
# @param options
|
|
172
175
|
# @return [HTTP::Client]
|
|
173
176
|
# @see Redirector#initialize
|
|
174
|
-
def follow(options = {})
|
|
177
|
+
def follow(options = {})
|
|
175
178
|
branch default_options.with_follow options
|
|
176
179
|
end
|
|
177
180
|
|
|
@@ -209,10 +212,11 @@ module HTTP
|
|
|
209
212
|
# @option opts [#to_s] :user
|
|
210
213
|
# @option opts [#to_s] :pass
|
|
211
214
|
def basic_auth(opts)
|
|
212
|
-
user
|
|
213
|
-
pass
|
|
215
|
+
user = opts.fetch(:user)
|
|
216
|
+
pass = opts.fetch(:pass)
|
|
217
|
+
creds = "#{user}:#{pass}"
|
|
214
218
|
|
|
215
|
-
auth("Basic
|
|
219
|
+
auth("Basic #{encode64(creds)}")
|
|
216
220
|
end
|
|
217
221
|
|
|
218
222
|
# Get options for HTTP
|
|
@@ -236,11 +240,37 @@ module HTTP
|
|
|
236
240
|
# Turn on given features. Available features are:
|
|
237
241
|
# * auto_inflate
|
|
238
242
|
# * auto_deflate
|
|
243
|
+
# * instrumentation
|
|
244
|
+
# * logging
|
|
245
|
+
# * normalize_uri
|
|
246
|
+
# * raise_error
|
|
239
247
|
# @param features
|
|
240
248
|
def use(*features)
|
|
241
249
|
branch default_options.with_features(features)
|
|
242
250
|
end
|
|
243
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
|
+
|
|
244
274
|
private
|
|
245
275
|
|
|
246
276
|
# :nodoc:
|
data/lib/http/client.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "forwardable"
|
|
|
4
4
|
|
|
5
5
|
require "http/form_data"
|
|
6
6
|
require "http/options"
|
|
7
|
+
require "http/feature"
|
|
7
8
|
require "http/headers"
|
|
8
9
|
require "http/connection"
|
|
9
10
|
require "http/redirector"
|
|
@@ -15,7 +16,7 @@ module HTTP
|
|
|
15
16
|
extend Forwardable
|
|
16
17
|
include Chainable
|
|
17
18
|
|
|
18
|
-
HTTP_OR_HTTPS_RE = %r{^https?://}i
|
|
19
|
+
HTTP_OR_HTTPS_RE = %r{^https?://}i.freeze
|
|
19
20
|
|
|
20
21
|
def initialize(default_options = {})
|
|
21
22
|
@default_options = HTTP::Options.new(default_options)
|
|
@@ -24,28 +25,34 @@ module HTTP
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# Make an HTTP request
|
|
27
|
-
def request(verb, uri, opts = {})
|
|
28
|
+
def request(verb, uri, opts = {})
|
|
29
|
+
opts = @default_options.merge(opts)
|
|
30
|
+
req = build_request(verb, uri, opts)
|
|
31
|
+
res = perform(req, opts)
|
|
32
|
+
return res unless opts.follow
|
|
33
|
+
|
|
34
|
+
Redirector.new(opts.follow).perform(req, res) do |request|
|
|
35
|
+
perform(wrap_request(request, opts), opts)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Prepare an HTTP request
|
|
40
|
+
def build_request(verb, uri, opts = {})
|
|
28
41
|
opts = @default_options.merge(opts)
|
|
29
42
|
uri = make_request_uri(uri, opts)
|
|
30
43
|
headers = make_request_headers(opts)
|
|
31
44
|
body = make_request_body(opts, headers)
|
|
32
|
-
proxy = opts.proxy
|
|
33
45
|
|
|
34
46
|
req = HTTP::Request.new(
|
|
35
|
-
:verb
|
|
36
|
-
:uri
|
|
37
|
-
:
|
|
38
|
-
:proxy
|
|
39
|
-
:
|
|
40
|
-
:
|
|
47
|
+
:verb => verb,
|
|
48
|
+
:uri => uri,
|
|
49
|
+
:uri_normalizer => opts.feature(:normalize_uri)&.normalizer,
|
|
50
|
+
:proxy => opts.proxy,
|
|
51
|
+
:headers => headers,
|
|
52
|
+
:body => body
|
|
41
53
|
)
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
return res unless opts.follow
|
|
45
|
-
|
|
46
|
-
Redirector.new(opts.follow).perform(req, res) do |request|
|
|
47
|
-
perform(request, opts)
|
|
48
|
-
end
|
|
55
|
+
wrap_request(req, opts)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
# @!method persistent?
|
|
@@ -59,23 +66,24 @@ module HTTP
|
|
|
59
66
|
|
|
60
67
|
@state = :dirty
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@connection.
|
|
66
|
-
|
|
69
|
+
begin
|
|
70
|
+
@connection ||= HTTP::Connection.new(req, options)
|
|
71
|
+
|
|
72
|
+
unless @connection.failed_proxy_connect?
|
|
73
|
+
@connection.send_request(req)
|
|
74
|
+
@connection.read_headers!
|
|
75
|
+
end
|
|
76
|
+
rescue Error => e
|
|
77
|
+
options.features.each_value do |feature|
|
|
78
|
+
feature.on_error(req, e)
|
|
79
|
+
end
|
|
80
|
+
raise
|
|
67
81
|
end
|
|
82
|
+
res = build_response(req, options)
|
|
68
83
|
|
|
69
|
-
res =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
:headers => @connection.headers,
|
|
73
|
-
:proxy_headers => @connection.proxy_response_headers,
|
|
74
|
-
:connection => @connection,
|
|
75
|
-
:encoding => options.encoding,
|
|
76
|
-
:auto_inflate => options.feature(:auto_inflate),
|
|
77
|
-
:uri => req.uri
|
|
78
|
-
)
|
|
84
|
+
res = options.features.inject(res) do |response, (_name, feature)|
|
|
85
|
+
feature.wrap_response(response)
|
|
86
|
+
end
|
|
79
87
|
|
|
80
88
|
@connection.finish_response if req.verb == :head
|
|
81
89
|
@state = :clean
|
|
@@ -87,26 +95,44 @@ module HTTP
|
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
def close
|
|
90
|
-
@connection
|
|
98
|
+
@connection&.close
|
|
91
99
|
@connection = nil
|
|
92
100
|
@state = :clean
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
private
|
|
96
104
|
|
|
105
|
+
def wrap_request(req, opts)
|
|
106
|
+
opts.features.inject(req) do |request, (_name, feature)|
|
|
107
|
+
feature.wrap_request(request)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_response(req, options)
|
|
112
|
+
Response.new(
|
|
113
|
+
:status => @connection.status_code,
|
|
114
|
+
:version => @connection.http_version,
|
|
115
|
+
:headers => @connection.headers,
|
|
116
|
+
:proxy_headers => @connection.proxy_response_headers,
|
|
117
|
+
:connection => @connection,
|
|
118
|
+
:encoding => options.encoding,
|
|
119
|
+
:request => req
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
97
123
|
# Verify our request isn't going to be made against another URI
|
|
98
124
|
def verify_connection!(uri)
|
|
99
125
|
if default_options.persistent? && uri.origin != default_options.persistent
|
|
100
126
|
raise StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{uri.origin}"
|
|
127
|
+
end
|
|
128
|
+
|
|
101
129
|
# We re-create the connection object because we want to let prior requests
|
|
102
130
|
# lazily load the body as long as possible, and this mimics prior functionality.
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
return close if @connection && (!@connection.keep_alive? || @connection.expired?)
|
|
132
|
+
|
|
105
133
|
# If we get into a bad state (eg, Timeout.timeout ensure being killed)
|
|
106
134
|
# close the connection to prevent potential for mixed responses.
|
|
107
|
-
|
|
108
|
-
close
|
|
109
|
-
end
|
|
135
|
+
return close if @state == :dirty
|
|
110
136
|
end
|
|
111
137
|
|
|
112
138
|
# Merges query params if needed
|
|
@@ -116,15 +142,11 @@ module HTTP
|
|
|
116
142
|
def make_request_uri(uri, opts)
|
|
117
143
|
uri = uri.to_s
|
|
118
144
|
|
|
119
|
-
if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE
|
|
120
|
-
uri = "#{default_options.persistent}#{uri}"
|
|
121
|
-
end
|
|
145
|
+
uri = "#{default_options.persistent}#{uri}" if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE
|
|
122
146
|
|
|
123
147
|
uri = HTTP::URI.parse uri
|
|
124
148
|
|
|
125
|
-
if opts.params && !opts.params.empty?
|
|
126
|
-
uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a)
|
|
127
|
-
end
|
|
149
|
+
uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a) if opts.params && !opts.params.empty?
|
|
128
150
|
|
|
129
151
|
# Some proxies (seen on WEBRick) fail if URL has
|
|
130
152
|
# empty path (e.g. `http://example.com`) while it's RFC-complaint:
|
|
@@ -148,14 +170,6 @@ module HTTP
|
|
|
148
170
|
headers[Headers::COOKIE] = cookies
|
|
149
171
|
end
|
|
150
172
|
|
|
151
|
-
if (auto_deflate = opts.feature(:auto_deflate))
|
|
152
|
-
# We need to delete Content-Length header. It will be set automatically
|
|
153
|
-
# by HTTP::Request::Writer
|
|
154
|
-
headers.delete(Headers::CONTENT_LENGTH)
|
|
155
|
-
|
|
156
|
-
headers[Headers::CONTENT_ENCODING] = auto_deflate.method
|
|
157
|
-
end
|
|
158
|
-
|
|
159
173
|
headers
|
|
160
174
|
end
|
|
161
175
|
|
|
@@ -165,14 +179,21 @@ module HTTP
|
|
|
165
179
|
when opts.body
|
|
166
180
|
opts.body
|
|
167
181
|
when opts.form
|
|
168
|
-
form =
|
|
182
|
+
form = make_form_data(opts.form)
|
|
169
183
|
headers[Headers::CONTENT_TYPE] ||= form.content_type
|
|
170
184
|
form
|
|
171
185
|
when opts.json
|
|
172
186
|
body = MimeType[:json].encode opts.json
|
|
173
|
-
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
|
|
187
|
+
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
|
|
174
188
|
body
|
|
175
189
|
end
|
|
176
190
|
end
|
|
191
|
+
|
|
192
|
+
def make_form_data(form)
|
|
193
|
+
return form if form.is_a? HTTP::FormData::Multipart
|
|
194
|
+
return form if form.is_a? HTTP::FormData::Urlencoded
|
|
195
|
+
|
|
196
|
+
HTTP::FormData.create(form)
|
|
197
|
+
end
|
|
177
198
|
end
|
|
178
199
|
end
|
data/lib/http/connection.rb
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
|
|
5
5
|
require "http/headers"
|
|
6
|
-
require "http/response/parser"
|
|
7
6
|
|
|
8
7
|
module HTTP
|
|
9
8
|
# A connection to the HTTP server
|
|
10
|
-
class Connection
|
|
9
|
+
class Connection
|
|
11
10
|
extend Forwardable
|
|
12
11
|
|
|
13
12
|
# Allowed values for CONNECTION header
|
|
@@ -35,6 +34,7 @@ module HTTP
|
|
|
35
34
|
@pending_request = false
|
|
36
35
|
@pending_response = false
|
|
37
36
|
@failed_proxy_connect = false
|
|
37
|
+
@buffer = "".b
|
|
38
38
|
|
|
39
39
|
@parser = Response::Parser.new
|
|
40
40
|
|
|
@@ -44,8 +44,11 @@ module HTTP
|
|
|
44
44
|
send_proxy_connect_request(req)
|
|
45
45
|
start_tls(req, options)
|
|
46
46
|
reset_timer
|
|
47
|
-
rescue IOError, SocketError, SystemCallError =>
|
|
48
|
-
raise ConnectionError, "failed to connect: #{
|
|
47
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
48
|
+
raise ConnectionError, "failed to connect: #{e}", e.backtrace
|
|
49
|
+
rescue TimeoutError
|
|
50
|
+
close
|
|
51
|
+
raise
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
# @see (HTTP::Response::Parser#status_code)
|
|
@@ -67,8 +70,13 @@ module HTTP
|
|
|
67
70
|
# @param [Request] req Request to send to the server
|
|
68
71
|
# @return [nil]
|
|
69
72
|
def send_request(req)
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
if @pending_response
|
|
74
|
+
raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if @pending_request
|
|
78
|
+
raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
|
|
79
|
+
end
|
|
72
80
|
|
|
73
81
|
@pending_request = true
|
|
74
82
|
|
|
@@ -92,19 +100,16 @@ module HTTP
|
|
|
92
100
|
chunk = @parser.read(size)
|
|
93
101
|
finish_response if finished
|
|
94
102
|
|
|
95
|
-
chunk.
|
|
103
|
+
chunk || "".b
|
|
96
104
|
end
|
|
97
105
|
|
|
98
106
|
# Reads data from socket up until headers are loaded
|
|
99
107
|
# @return [void]
|
|
108
|
+
# @raise [ResponseHeaderError] when unable to read response headers
|
|
100
109
|
def read_headers!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
break
|
|
105
|
-
elsif @parser.headers?
|
|
106
|
-
break
|
|
107
|
-
end
|
|
110
|
+
until @parser.headers?
|
|
111
|
+
result = read_more(BUFFER_SIZE)
|
|
112
|
+
raise ResponseHeaderError, "couldn't read response headers" if result == :eof
|
|
108
113
|
end
|
|
109
114
|
|
|
110
115
|
set_keep_alive
|
|
@@ -125,12 +130,16 @@ module HTTP
|
|
|
125
130
|
# Close the connection
|
|
126
131
|
# @return [void]
|
|
127
132
|
def close
|
|
128
|
-
@socket.close unless @socket
|
|
133
|
+
@socket.close unless @socket&.closed?
|
|
129
134
|
|
|
130
135
|
@pending_response = false
|
|
131
136
|
@pending_request = false
|
|
132
137
|
end
|
|
133
138
|
|
|
139
|
+
def finished_request?
|
|
140
|
+
!@pending_request && !@pending_response
|
|
141
|
+
end
|
|
142
|
+
|
|
134
143
|
# Whether we're keeping the conn alive
|
|
135
144
|
# @return [Boolean]
|
|
136
145
|
def keep_alive?
|
|
@@ -209,18 +218,19 @@ module HTTP
|
|
|
209
218
|
|
|
210
219
|
# Feeds some more data into parser
|
|
211
220
|
# @return [void]
|
|
221
|
+
# @raise [SocketReadError] when unable to read from socket
|
|
212
222
|
def read_more(size)
|
|
213
223
|
return if @parser.finished?
|
|
214
224
|
|
|
215
|
-
value = @socket.readpartial(size)
|
|
225
|
+
value = @socket.readpartial(size, @buffer)
|
|
216
226
|
if value == :eof
|
|
217
227
|
@parser << ""
|
|
218
228
|
:eof
|
|
219
229
|
elsif value
|
|
220
230
|
@parser << value
|
|
221
231
|
end
|
|
222
|
-
rescue IOError, SocketError, SystemCallError =>
|
|
223
|
-
raise
|
|
232
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
233
|
+
raise SocketReadError, "error reading from socket: #{e}", e.backtrace
|
|
224
234
|
end
|
|
225
235
|
end
|
|
226
236
|
end
|
data/lib/http/content_type.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
|
-
ContentType
|
|
5
|
-
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
|
6
|
-
CHARSET_RE = /;\s*charset=([^;]+)/i
|
|
4
|
+
class ContentType
|
|
5
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
|
|
6
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
|
|
7
|
+
|
|
8
|
+
attr_accessor :mime_type, :charset
|
|
7
9
|
|
|
8
10
|
class << self
|
|
9
11
|
# Parse string and return ContentType struct
|
|
@@ -15,15 +17,18 @@ module HTTP
|
|
|
15
17
|
|
|
16
18
|
# :nodoc:
|
|
17
19
|
def mime_type(str)
|
|
18
|
-
|
|
19
|
-
m && m.strip.downcase
|
|
20
|
+
str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
# :nodoc:
|
|
23
24
|
def charset(str)
|
|
24
|
-
|
|
25
|
-
m && m.strip.delete('"')
|
|
25
|
+
str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
def initialize(mime_type = nil, charset = nil)
|
|
30
|
+
@mime_type = mime_type
|
|
31
|
+
@charset = charset
|
|
32
|
+
end
|
|
28
33
|
end
|
|
29
34
|
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,9 +21,23 @@ 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
|
|
|
38
|
+
# Timeout when first establishing the conncetion
|
|
39
|
+
class ConnectTimeoutError < TimeoutError; end
|
|
40
|
+
|
|
22
41
|
# Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
|
|
23
42
|
class HeaderError < Error; end
|
|
24
43
|
end
|