http 5.3.1 → 6.0.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.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
data/lib/http/connection.rb
CHANGED
|
@@ -2,20 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
|
|
5
|
+
require "http/connection/internals"
|
|
5
6
|
require "http/headers"
|
|
6
7
|
|
|
7
8
|
module HTTP
|
|
8
9
|
# A connection to the HTTP server
|
|
9
10
|
class Connection
|
|
10
11
|
extend Forwardable
|
|
12
|
+
include Internals
|
|
11
13
|
|
|
12
14
|
# Allowed values for CONNECTION header
|
|
13
15
|
KEEP_ALIVE = "Keep-Alive"
|
|
16
|
+
# Connection: close header value
|
|
14
17
|
CLOSE = "close"
|
|
15
18
|
|
|
16
19
|
# Attempt to read this much data
|
|
17
20
|
BUFFER_SIZE = 16_384
|
|
18
21
|
|
|
22
|
+
# Maximum response body size (in bytes) to auto-flush when reusing
|
|
23
|
+
# a connection. Bodies larger than this cause the connection to close
|
|
24
|
+
# instead, to avoid blocking on huge downloads.
|
|
25
|
+
MAX_FLUSH_SIZE = 1_048_576
|
|
26
|
+
|
|
19
27
|
# HTTP/1.0
|
|
20
28
|
HTTP_1_0 = "1.0"
|
|
21
29
|
|
|
@@ -23,27 +31,30 @@ module HTTP
|
|
|
23
31
|
HTTP_1_1 = "1.1"
|
|
24
32
|
|
|
25
33
|
# Returned after HTTP CONNECT (via proxy)
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# connection.proxy_response_headers
|
|
37
|
+
#
|
|
38
|
+
# @return [HTTP::Headers, nil]
|
|
39
|
+
# @api public
|
|
26
40
|
attr_reader :proxy_response_headers
|
|
27
41
|
|
|
42
|
+
# Initialize a new connection to an HTTP server
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Connection.new(req, options)
|
|
46
|
+
#
|
|
28
47
|
# @param [HTTP::Request] req
|
|
29
48
|
# @param [HTTP::Options] options
|
|
49
|
+
# @return [Connection]
|
|
30
50
|
# @raise [HTTP::ConnectionError] when failed to connect
|
|
51
|
+
# @api public
|
|
31
52
|
def initialize(req, options)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@buffer = "".b
|
|
38
|
-
|
|
39
|
-
@parser = Response::Parser.new
|
|
40
|
-
|
|
41
|
-
@socket = options.timeout_class.new(options.timeout_options)
|
|
42
|
-
@socket.connect(options.socket_class, req.socket_host, req.socket_port, options.nodelay)
|
|
43
|
-
|
|
44
|
-
send_proxy_connect_request(req)
|
|
45
|
-
start_tls(req, options)
|
|
46
|
-
reset_timer
|
|
53
|
+
init_state(options)
|
|
54
|
+
connect_socket(req, options)
|
|
55
|
+
rescue IO::TimeoutError => e
|
|
56
|
+
close
|
|
57
|
+
raise ConnectTimeoutError, e.message, e.backtrace
|
|
47
58
|
rescue IOError, SocketError, SystemCallError => e
|
|
48
59
|
raise ConnectionError, "failed to connect: #{e}", e.backtrace
|
|
49
60
|
rescue TimeoutError
|
|
@@ -60,19 +71,37 @@ module HTTP
|
|
|
60
71
|
# @see (HTTP::Response::Parser#headers)
|
|
61
72
|
def_delegator :@parser, :headers
|
|
62
73
|
|
|
74
|
+
# Whether the proxy CONNECT request failed
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# connection.failed_proxy_connect?
|
|
78
|
+
#
|
|
63
79
|
# @return [Boolean] whenever proxy connect failed
|
|
80
|
+
# @api public
|
|
64
81
|
def failed_proxy_connect?
|
|
65
82
|
@failed_proxy_connect
|
|
66
83
|
end
|
|
67
84
|
|
|
85
|
+
# Set the pending response for auto-flushing before the next request
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# connection.pending_response = response
|
|
89
|
+
#
|
|
90
|
+
# @param [HTTP::Response, false] response
|
|
91
|
+
# @return [void]
|
|
92
|
+
# @api public
|
|
93
|
+
attr_writer :pending_response
|
|
94
|
+
|
|
68
95
|
# Send a request to the server
|
|
69
96
|
#
|
|
97
|
+
# @example
|
|
98
|
+
# connection.send_request(req)
|
|
99
|
+
#
|
|
70
100
|
# @param [Request] req Request to send to the server
|
|
71
101
|
# @return [nil]
|
|
102
|
+
# @api public
|
|
72
103
|
def send_request(req)
|
|
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
|
|
104
|
+
flush_pending_response if @pending_response
|
|
76
105
|
|
|
77
106
|
if @pending_request
|
|
78
107
|
raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
|
|
@@ -88,24 +117,37 @@ module HTTP
|
|
|
88
117
|
|
|
89
118
|
# Read a chunk of the body
|
|
90
119
|
#
|
|
120
|
+
# @example
|
|
121
|
+
# connection.readpartial
|
|
122
|
+
#
|
|
123
|
+
# @param [Integer] size maximum bytes to read
|
|
124
|
+
# @param [String, nil] outbuf buffer to fill with data
|
|
91
125
|
# @return [String] data chunk
|
|
92
|
-
# @
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
# @raise [EOFError] when no more data left
|
|
127
|
+
# @api public
|
|
128
|
+
def readpartial(size = BUFFER_SIZE, outbuf = nil)
|
|
129
|
+
raise EOFError unless @pending_response
|
|
95
130
|
|
|
96
131
|
chunk = @parser.read(size)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
unless chunk
|
|
133
|
+
eof = read_more(size) == :eof
|
|
134
|
+
check_premature_eof(eof)
|
|
135
|
+
finished = eof || @parser.finished?
|
|
136
|
+
chunk = @parser.read(size) || "".b
|
|
137
|
+
finish_response if finished
|
|
138
|
+
end
|
|
102
139
|
|
|
103
|
-
chunk
|
|
140
|
+
outbuf ? outbuf.replace(chunk) : chunk
|
|
104
141
|
end
|
|
105
142
|
|
|
106
143
|
# Reads data from socket up until headers are loaded
|
|
144
|
+
#
|
|
145
|
+
# @example
|
|
146
|
+
# connection.read_headers!
|
|
147
|
+
#
|
|
107
148
|
# @return [void]
|
|
108
149
|
# @raise [ResponseHeaderError] when unable to read response headers
|
|
150
|
+
# @api public
|
|
109
151
|
def read_headers!
|
|
110
152
|
until @parser.headers?
|
|
111
153
|
result = read_more(BUFFER_SIZE)
|
|
@@ -116,7 +158,12 @@ module HTTP
|
|
|
116
158
|
end
|
|
117
159
|
|
|
118
160
|
# Callback for when we've reached the end of a response
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# connection.finish_response
|
|
164
|
+
#
|
|
119
165
|
# @return [void]
|
|
166
|
+
# @api public
|
|
120
167
|
def finish_response
|
|
121
168
|
close unless keep_alive?
|
|
122
169
|
|
|
@@ -128,7 +175,12 @@ module HTTP
|
|
|
128
175
|
end
|
|
129
176
|
|
|
130
177
|
# Close the connection
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# connection.close
|
|
181
|
+
#
|
|
131
182
|
# @return [void]
|
|
183
|
+
# @api public
|
|
132
184
|
def close
|
|
133
185
|
@socket.close unless @socket&.closed?
|
|
134
186
|
|
|
@@ -136,101 +188,78 @@ module HTTP
|
|
|
136
188
|
@pending_request = false
|
|
137
189
|
end
|
|
138
190
|
|
|
191
|
+
# Whether there are no pending requests or responses
|
|
192
|
+
#
|
|
193
|
+
# @example
|
|
194
|
+
# connection.finished_request?
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
# @api public
|
|
139
198
|
def finished_request?
|
|
140
199
|
!@pending_request && !@pending_response
|
|
141
200
|
end
|
|
142
201
|
|
|
143
202
|
# Whether we're keeping the conn alive
|
|
203
|
+
#
|
|
204
|
+
# @example
|
|
205
|
+
# connection.keep_alive?
|
|
206
|
+
#
|
|
144
207
|
# @return [Boolean]
|
|
208
|
+
# @api public
|
|
145
209
|
def keep_alive?
|
|
146
|
-
|
|
210
|
+
@keep_alive && !@socket.closed?
|
|
147
211
|
end
|
|
148
212
|
|
|
149
213
|
# Whether our connection has expired
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# connection.expired?
|
|
217
|
+
#
|
|
150
218
|
# @return [Boolean]
|
|
219
|
+
# @api public
|
|
151
220
|
def expired?
|
|
152
221
|
!@conn_expires_at || @conn_expires_at < Time.now
|
|
153
222
|
end
|
|
154
223
|
|
|
155
224
|
private
|
|
156
225
|
|
|
157
|
-
#
|
|
158
|
-
# @param (see #initialize)
|
|
226
|
+
# Initialize connection state
|
|
159
227
|
# @return [void]
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Open tunnel through proxy
|
|
174
|
-
def send_proxy_connect_request(req)
|
|
175
|
-
return unless req.uri.https? && req.using_proxy?
|
|
176
|
-
|
|
177
|
-
@pending_request = true
|
|
178
|
-
|
|
179
|
-
req.connect_using_proxy @socket
|
|
180
|
-
|
|
181
|
-
@pending_request = false
|
|
182
|
-
@pending_response = true
|
|
183
|
-
|
|
184
|
-
read_headers!
|
|
185
|
-
@proxy_response_headers = @parser.headers
|
|
186
|
-
|
|
187
|
-
if @parser.status_code != 200
|
|
188
|
-
@failed_proxy_connect = true
|
|
189
|
-
return
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
@parser.reset
|
|
193
|
-
@pending_response = false
|
|
228
|
+
# @api private
|
|
229
|
+
def init_state(options)
|
|
230
|
+
@persistent = options.persistent?
|
|
231
|
+
@keep_alive_timeout = options.keep_alive_timeout.to_f
|
|
232
|
+
@pending_request = false
|
|
233
|
+
@pending_response = false
|
|
234
|
+
@failed_proxy_connect = false
|
|
235
|
+
@buffer = "".b
|
|
236
|
+
@parser = Response::Parser.new
|
|
194
237
|
end
|
|
195
238
|
|
|
196
|
-
#
|
|
239
|
+
# Check for premature end-of-file and raise if detected
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# check_premature_eof(:eof)
|
|
243
|
+
#
|
|
197
244
|
# @return [void]
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
245
|
+
# @api private
|
|
246
|
+
def check_premature_eof(eof)
|
|
247
|
+
return unless eof && !@parser.finished? && body_framed?
|
|
201
248
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# @return [void]
|
|
205
|
-
def set_keep_alive
|
|
206
|
-
return @keep_alive = false unless @persistent
|
|
207
|
-
|
|
208
|
-
@keep_alive =
|
|
209
|
-
case @parser.http_version
|
|
210
|
-
when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
|
|
211
|
-
@parser.headers[Headers::CONNECTION] == KEEP_ALIVE
|
|
212
|
-
when HTTP_1_1 # HTTP/1.1 is opt-out
|
|
213
|
-
@parser.headers[Headers::CONNECTION] != CLOSE
|
|
214
|
-
else # Anything else we assume doesn't supportit
|
|
215
|
-
false
|
|
216
|
-
end
|
|
249
|
+
close
|
|
250
|
+
raise ConnectionError, "response body ended prematurely"
|
|
217
251
|
end
|
|
218
252
|
|
|
219
|
-
#
|
|
253
|
+
# Connect socket and set up proxy/TLS
|
|
220
254
|
# @return [void]
|
|
221
|
-
# @
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
elsif value
|
|
230
|
-
@parser << value
|
|
231
|
-
end
|
|
232
|
-
rescue IOError, SocketError, SystemCallError => e
|
|
233
|
-
raise SocketReadError, "error reading from socket: #{e}", e.backtrace
|
|
255
|
+
# @api private
|
|
256
|
+
def connect_socket(req, options)
|
|
257
|
+
@socket = options.timeout_class.new(**options.timeout_options) # steep:ignore
|
|
258
|
+
@socket.connect(options.socket_class, req.socket_host, req.socket_port, nodelay: options.nodelay)
|
|
259
|
+
|
|
260
|
+
send_proxy_connect_request(req)
|
|
261
|
+
start_tls(req, options)
|
|
262
|
+
reset_timer
|
|
234
263
|
end
|
|
235
264
|
end
|
|
236
265
|
end
|
data/lib/http/content_type.rb
CHANGED
|
@@ -1,34 +1,89 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
|
+
# Parsed representation of a Content-Type header
|
|
4
5
|
class ContentType
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
# Pattern for extracting MIME type from Content-Type header
|
|
7
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
|
8
|
+
# Pattern for extracting charset from Content-Type header
|
|
9
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
# MIME type of the content
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# content_type.mime_type # => "text/html"
|
|
15
|
+
#
|
|
16
|
+
# @return [String, nil]
|
|
17
|
+
# @api public
|
|
18
|
+
attr_accessor :mime_type
|
|
19
|
+
|
|
20
|
+
# Character set of the content
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# content_type.charset # => "utf-8"
|
|
24
|
+
#
|
|
25
|
+
# @return [String, nil]
|
|
26
|
+
# @api public
|
|
27
|
+
attr_accessor :charset
|
|
9
28
|
|
|
10
29
|
class << self
|
|
11
|
-
# Parse string and return ContentType
|
|
30
|
+
# Parse string and return ContentType object
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# HTTP::ContentType.parse("text/html; charset=utf-8")
|
|
34
|
+
#
|
|
35
|
+
# @param [String] str content type header value
|
|
36
|
+
# @return [ContentType]
|
|
37
|
+
# @api public
|
|
12
38
|
def parse(str)
|
|
13
39
|
new mime_type(str), charset(str)
|
|
14
40
|
end
|
|
15
41
|
|
|
16
42
|
private
|
|
17
43
|
|
|
18
|
-
#
|
|
44
|
+
# Extract MIME type from header string
|
|
45
|
+
# @return [String, nil]
|
|
46
|
+
# @api private
|
|
19
47
|
def mime_type(str)
|
|
20
48
|
str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
|
|
21
49
|
end
|
|
22
50
|
|
|
23
|
-
#
|
|
51
|
+
# Extract charset from header string
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
# @api private
|
|
24
54
|
def charset(str)
|
|
25
55
|
str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
|
|
26
56
|
end
|
|
27
57
|
end
|
|
28
58
|
|
|
59
|
+
# Create a new ContentType instance
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# HTTP::ContentType.new("text/html", "utf-8")
|
|
63
|
+
#
|
|
64
|
+
# @param [String, nil] mime_type MIME type
|
|
65
|
+
# @param [String, nil] charset character set
|
|
66
|
+
# @return [ContentType]
|
|
67
|
+
# @api public
|
|
29
68
|
def initialize(mime_type = nil, charset = nil)
|
|
30
69
|
@mime_type = mime_type
|
|
31
70
|
@charset = charset
|
|
32
71
|
end
|
|
72
|
+
|
|
73
|
+
# Pattern matching interface for matching against content type attributes
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# case response.content_type
|
|
77
|
+
# in { mime_type: /json/ }
|
|
78
|
+
# "JSON content"
|
|
79
|
+
# end
|
|
80
|
+
#
|
|
81
|
+
# @param keys [Array<Symbol>, nil] keys to extract, or nil for all
|
|
82
|
+
# @return [Hash{Symbol => Object}]
|
|
83
|
+
# @api public
|
|
84
|
+
def deconstruct_keys(keys)
|
|
85
|
+
hash = { mime_type: @mime_type, charset: @charset }
|
|
86
|
+
keys ? hash.slice(*keys) : hash
|
|
87
|
+
end
|
|
33
88
|
end
|
|
34
89
|
end
|
data/lib/http/errors.rb
CHANGED
|
@@ -9,7 +9,9 @@ module HTTP
|
|
|
9
9
|
|
|
10
10
|
# Types of Connection errors
|
|
11
11
|
class ResponseHeaderError < ConnectionError; end
|
|
12
|
+
# Error raised when reading from a socket fails
|
|
12
13
|
class SocketReadError < ConnectionError; end
|
|
14
|
+
# Error raised when writing to a socket fails
|
|
13
15
|
class SocketWriteError < ConnectionError; end
|
|
14
16
|
|
|
15
17
|
# Generic Request error
|
|
@@ -23,8 +25,23 @@ module HTTP
|
|
|
23
25
|
|
|
24
26
|
# When status code indicates an error
|
|
25
27
|
class StatusError < ResponseError
|
|
28
|
+
# The HTTP response that caused the error
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# error.response
|
|
32
|
+
#
|
|
33
|
+
# @return [HTTP::Response]
|
|
34
|
+
# @api public
|
|
26
35
|
attr_reader :response
|
|
27
36
|
|
|
37
|
+
# Create a new StatusError from a response
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# HTTP::StatusError.new(response)
|
|
41
|
+
#
|
|
42
|
+
# @param [HTTP::Response] response the response with error status
|
|
43
|
+
# @return [StatusError]
|
|
44
|
+
# @api public
|
|
28
45
|
def initialize(response)
|
|
29
46
|
@response = response
|
|
30
47
|
|
|
@@ -32,10 +49,17 @@ module HTTP
|
|
|
32
49
|
end
|
|
33
50
|
end
|
|
34
51
|
|
|
52
|
+
# Raised when `Response#parse` fails due to any underlying reason (unexpected
|
|
53
|
+
# MIME type, or decoder fails). See `Exception#cause` for the original exception.
|
|
54
|
+
class ParseError < ResponseError; end
|
|
55
|
+
|
|
56
|
+
# Requested MimeType adapter not found.
|
|
57
|
+
class UnsupportedMimeTypeError < Error; end
|
|
58
|
+
|
|
35
59
|
# Generic Timeout error
|
|
36
60
|
class TimeoutError < Error; end
|
|
37
61
|
|
|
38
|
-
# Timeout when first establishing the
|
|
62
|
+
# Timeout when first establishing the connection
|
|
39
63
|
class ConnectTimeoutError < TimeoutError; end
|
|
40
64
|
|
|
41
65
|
# Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
|
data/lib/http/feature.rb
CHANGED
|
@@ -1,25 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
|
+
# Base class for HTTP client features (middleware)
|
|
4
5
|
class Feature
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# Wraps an HTTP request
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# feature.wrap_request(request)
|
|
10
|
+
#
|
|
11
|
+
# @param request [HTTP::Request]
|
|
12
|
+
# @return [HTTP::Request]
|
|
13
|
+
# @api public
|
|
9
14
|
def wrap_request(request)
|
|
10
15
|
request
|
|
11
16
|
end
|
|
12
17
|
|
|
18
|
+
# Wraps an HTTP response
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# feature.wrap_response(response)
|
|
22
|
+
#
|
|
23
|
+
# @param response [HTTP::Response]
|
|
24
|
+
# @return [HTTP::Response]
|
|
25
|
+
# @api public
|
|
13
26
|
def wrap_response(response)
|
|
14
27
|
response
|
|
15
28
|
end
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
# Callback invoked before each request attempt
|
|
31
|
+
#
|
|
32
|
+
# Unlike {#wrap_request}, which is called once when the request is built,
|
|
33
|
+
# this hook is called before every attempt, including retries. Use it for
|
|
34
|
+
# per-attempt side effects like starting instrumentation spans.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# feature.on_request(request)
|
|
38
|
+
#
|
|
39
|
+
# @param _request [HTTP::Request]
|
|
40
|
+
# @return [nil]
|
|
41
|
+
# @api public
|
|
42
|
+
def on_request(_request); end
|
|
43
|
+
|
|
44
|
+
# Wraps the HTTP exchange for a single request attempt
|
|
45
|
+
#
|
|
46
|
+
# Called once per attempt (including retries), wrapping the send and
|
|
47
|
+
# receive cycle. The block performs the I/O and returns the response.
|
|
48
|
+
# Override this to add behavior that must span the entire exchange,
|
|
49
|
+
# such as instrumentation spans or circuit breakers.
|
|
50
|
+
#
|
|
51
|
+
# @example Timing a request
|
|
52
|
+
# def around_request(request)
|
|
53
|
+
# start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
54
|
+
# yield(request).tap { log_duration(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) }
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# @param request [HTTP::Request]
|
|
58
|
+
# @yield [HTTP::Request] the request to perform
|
|
59
|
+
# @yieldreturn [HTTP::Response]
|
|
60
|
+
# @return [HTTP::Response] must return the response from yield
|
|
61
|
+
# @api public
|
|
62
|
+
def around_request(request)
|
|
63
|
+
yield request
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Callback for request errors
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# feature.on_error(request, error)
|
|
70
|
+
#
|
|
71
|
+
# @param _request [HTTP::Request]
|
|
72
|
+
# @param _error [Exception]
|
|
73
|
+
# @return [nil]
|
|
74
|
+
# @api public
|
|
75
|
+
def on_error(_request, _error); end
|
|
18
76
|
end
|
|
19
77
|
end
|
|
20
78
|
|
|
21
79
|
require "http/features/auto_inflate"
|
|
22
80
|
require "http/features/auto_deflate"
|
|
81
|
+
require "http/features/caching"
|
|
82
|
+
require "http/features/digest_auth"
|
|
23
83
|
require "http/features/instrumentation"
|
|
24
84
|
require "http/features/logging"
|
|
25
85
|
require "http/features/normalize_uri"
|