http 6.0.0-java
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 +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- 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 +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- 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 +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- 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/capture_warning.rb +10 -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/test/support/fakeio.rb +21 -0
- 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/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +218 -0
data/lib/http/request.rb
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
require "http/base64"
|
|
7
|
+
require "http/errors"
|
|
8
|
+
require "http/headers"
|
|
9
|
+
require "http/request/body"
|
|
10
|
+
require "http/request/proxy"
|
|
11
|
+
require "http/request/writer"
|
|
12
|
+
require "http/version"
|
|
13
|
+
require "http/uri"
|
|
14
|
+
|
|
15
|
+
module HTTP
|
|
16
|
+
# Represents an HTTP request with verb, URI, headers, and body
|
|
17
|
+
class Request
|
|
18
|
+
extend Forwardable
|
|
19
|
+
|
|
20
|
+
include HTTP::Base64
|
|
21
|
+
include Proxy
|
|
22
|
+
|
|
23
|
+
# The method given was not understood
|
|
24
|
+
class UnsupportedMethodError < RequestError; end
|
|
25
|
+
|
|
26
|
+
# The scheme of given URI was not understood
|
|
27
|
+
class UnsupportedSchemeError < RequestError; end
|
|
28
|
+
|
|
29
|
+
# Default User-Agent header value
|
|
30
|
+
USER_AGENT = "http.rb/#{HTTP::VERSION}".freeze
|
|
31
|
+
|
|
32
|
+
# Supported HTTP methods
|
|
33
|
+
METHODS = [
|
|
34
|
+
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
|
|
35
|
+
:options, :get, :head, :post, :put, :delete, :trace, :connect,
|
|
36
|
+
|
|
37
|
+
# RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV
|
|
38
|
+
:propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock,
|
|
39
|
+
|
|
40
|
+
# RFC 3648: WebDAV Ordered Collections Protocol
|
|
41
|
+
:orderpatch,
|
|
42
|
+
|
|
43
|
+
# RFC 3744: WebDAV Access Control Protocol
|
|
44
|
+
:acl,
|
|
45
|
+
|
|
46
|
+
# RFC 6352: vCard Extensions to WebDAV -- CardDAV
|
|
47
|
+
:report,
|
|
48
|
+
|
|
49
|
+
# RFC 5789: PATCH Method for HTTP
|
|
50
|
+
:patch,
|
|
51
|
+
|
|
52
|
+
# draft-reschke-webdav-search: WebDAV Search
|
|
53
|
+
:search,
|
|
54
|
+
|
|
55
|
+
# RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
|
|
56
|
+
:mkcalendar,
|
|
57
|
+
|
|
58
|
+
# Implemented by several caching servers, like Squid, Varnish or Fastly
|
|
59
|
+
:purge
|
|
60
|
+
].freeze
|
|
61
|
+
|
|
62
|
+
# Allowed schemes
|
|
63
|
+
SCHEMES = %i[http https ws wss].freeze
|
|
64
|
+
|
|
65
|
+
# Default ports of supported schemes
|
|
66
|
+
PORTS = {
|
|
67
|
+
http: 80,
|
|
68
|
+
https: 443,
|
|
69
|
+
ws: 80,
|
|
70
|
+
wss: 443
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
# HTTP method as a lowercase symbol
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# request.verb # => :get
|
|
77
|
+
#
|
|
78
|
+
# @return [Symbol]
|
|
79
|
+
# @api public
|
|
80
|
+
attr_reader :verb
|
|
81
|
+
|
|
82
|
+
# URI scheme as a lowercase symbol
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# request.scheme # => :https
|
|
86
|
+
#
|
|
87
|
+
# @return [Symbol]
|
|
88
|
+
# @api public
|
|
89
|
+
attr_reader :scheme
|
|
90
|
+
|
|
91
|
+
# URI normalizer callable
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# request.uri_normalizer
|
|
95
|
+
#
|
|
96
|
+
# @return [#call]
|
|
97
|
+
# @api public
|
|
98
|
+
attr_reader :uri_normalizer
|
|
99
|
+
|
|
100
|
+
# Request URI
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# request.uri # => #<HTTP::URI ...>
|
|
104
|
+
#
|
|
105
|
+
# @return [HTTP::URI]
|
|
106
|
+
# @api public
|
|
107
|
+
attr_reader :uri
|
|
108
|
+
|
|
109
|
+
# Proxy configuration hash
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# request.proxy
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash]
|
|
115
|
+
# @api public
|
|
116
|
+
attr_reader :proxy
|
|
117
|
+
|
|
118
|
+
# Request body object
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# request.body
|
|
122
|
+
#
|
|
123
|
+
# @return [HTTP::Request::Body]
|
|
124
|
+
# @api public
|
|
125
|
+
attr_reader :body
|
|
126
|
+
|
|
127
|
+
# The HTTP headers collection
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# request.headers
|
|
131
|
+
#
|
|
132
|
+
# @return [HTTP::Headers]
|
|
133
|
+
# @api public
|
|
134
|
+
attr_reader :headers
|
|
135
|
+
|
|
136
|
+
# HTTP version string
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# request.version # => "1.1"
|
|
140
|
+
#
|
|
141
|
+
# @return [String]
|
|
142
|
+
# @api public
|
|
143
|
+
attr_reader :version
|
|
144
|
+
|
|
145
|
+
# Create a new HTTP request
|
|
146
|
+
#
|
|
147
|
+
# @param [#to_s] verb HTTP request method
|
|
148
|
+
# @param [HTTP::URI, #to_s] uri
|
|
149
|
+
# @param [Hash] headers
|
|
150
|
+
# @param [Hash] proxy
|
|
151
|
+
# @param [String, Enumerable, IO, nil] body
|
|
152
|
+
# @param [String] version
|
|
153
|
+
# @param [#call] uri_normalizer
|
|
154
|
+
#
|
|
155
|
+
# @example
|
|
156
|
+
# Request.new(verb: :get, uri: "https://example.com")
|
|
157
|
+
#
|
|
158
|
+
# @return [HTTP::Request]
|
|
159
|
+
# @api public
|
|
160
|
+
def initialize(verb:, uri:, headers: nil, proxy: {}, body: nil, version: "1.1",
|
|
161
|
+
uri_normalizer: nil)
|
|
162
|
+
@uri_normalizer = uri_normalizer || HTTP::URI::NORMALIZER
|
|
163
|
+
@verb = verb.to_s.downcase.to_sym
|
|
164
|
+
parse_uri!(uri)
|
|
165
|
+
validate_method_and_scheme!
|
|
166
|
+
|
|
167
|
+
@proxy = proxy
|
|
168
|
+
@version = version
|
|
169
|
+
@headers = prepare_headers(headers)
|
|
170
|
+
@body = prepare_body(body)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns new Request with updated uri
|
|
174
|
+
#
|
|
175
|
+
# @example
|
|
176
|
+
# request.redirect("https://example.com/new")
|
|
177
|
+
#
|
|
178
|
+
# @return [HTTP::Request]
|
|
179
|
+
# @api public
|
|
180
|
+
def redirect(uri, verb = @verb)
|
|
181
|
+
redirect_uri = @uri.join(uri)
|
|
182
|
+
headers = redirect_headers(redirect_uri, verb)
|
|
183
|
+
new_body = verb == :get ? nil : body.source
|
|
184
|
+
|
|
185
|
+
self.class.new(
|
|
186
|
+
verb: verb,
|
|
187
|
+
uri: redirect_uri,
|
|
188
|
+
headers: headers,
|
|
189
|
+
proxy: proxy,
|
|
190
|
+
body: new_body,
|
|
191
|
+
version: version,
|
|
192
|
+
uri_normalizer: uri_normalizer
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Stream the request to a socket
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# request.stream(socket)
|
|
200
|
+
#
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @api public
|
|
203
|
+
def stream(socket)
|
|
204
|
+
include_proxy_headers if using_proxy? && !@uri.https?
|
|
205
|
+
Request::Writer.new(socket, body, headers, headline).stream
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Is this request using a proxy?
|
|
209
|
+
#
|
|
210
|
+
# @example
|
|
211
|
+
# request.using_proxy?
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
# @api public
|
|
215
|
+
def using_proxy?
|
|
216
|
+
proxy && proxy.keys.size >= 2
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Is this request using an authenticated proxy?
|
|
220
|
+
#
|
|
221
|
+
# @example
|
|
222
|
+
# request.using_authenticated_proxy?
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean]
|
|
225
|
+
# @api public
|
|
226
|
+
def using_authenticated_proxy?
|
|
227
|
+
proxy && proxy.keys.size >= 4
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Compute HTTP request header for direct or proxy request
|
|
231
|
+
#
|
|
232
|
+
# @example
|
|
233
|
+
# request.headline
|
|
234
|
+
#
|
|
235
|
+
# @return [String]
|
|
236
|
+
# @api public
|
|
237
|
+
def headline
|
|
238
|
+
request_uri =
|
|
239
|
+
if using_proxy? && !uri.https?
|
|
240
|
+
uri.omit(:fragment)
|
|
241
|
+
else
|
|
242
|
+
uri.request_uri
|
|
243
|
+
end.to_s
|
|
244
|
+
|
|
245
|
+
raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
|
|
246
|
+
|
|
247
|
+
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Host for tcp socket
|
|
251
|
+
#
|
|
252
|
+
# @example
|
|
253
|
+
# request.socket_host
|
|
254
|
+
#
|
|
255
|
+
# @return [String]
|
|
256
|
+
# @api public
|
|
257
|
+
def socket_host
|
|
258
|
+
using_proxy? ? proxy[:proxy_address] : host
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Port for tcp socket
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# request.socket_port
|
|
265
|
+
#
|
|
266
|
+
# @return [Integer]
|
|
267
|
+
# @api public
|
|
268
|
+
def socket_port
|
|
269
|
+
using_proxy? ? proxy[:proxy_port] : port
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Human-readable representation of base request info
|
|
273
|
+
#
|
|
274
|
+
# @example
|
|
275
|
+
#
|
|
276
|
+
# req.inspect
|
|
277
|
+
# # => #<HTTP::Request/1.1 GET https://example.com>
|
|
278
|
+
#
|
|
279
|
+
# @return [String]
|
|
280
|
+
# @api public
|
|
281
|
+
def inspect
|
|
282
|
+
format("#<%s/%s %s %s>", self.class, @version, String(verb).upcase, uri)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# Build headers for a redirect request
|
|
288
|
+
#
|
|
289
|
+
# Strips the Host header (it will be regenerated), sensitive credentials
|
|
290
|
+
# on cross-origin redirects, and Content-Type on GET verb changes.
|
|
291
|
+
#
|
|
292
|
+
# @param [HTTP::URI] redirect_uri the target URI
|
|
293
|
+
# @param [Symbol] verb the HTTP verb for the redirected request
|
|
294
|
+
# @return [HTTP::Headers] headers for the redirect
|
|
295
|
+
# @api private
|
|
296
|
+
def redirect_headers(redirect_uri, verb)
|
|
297
|
+
headers = self.headers.dup
|
|
298
|
+
headers.delete(Headers::HOST)
|
|
299
|
+
|
|
300
|
+
# Strip sensitive headers when redirecting to a different origin
|
|
301
|
+
# (scheme + host + port) to prevent credential leakage.
|
|
302
|
+
unless @uri.origin == redirect_uri.origin
|
|
303
|
+
headers.delete(Headers::AUTHORIZATION)
|
|
304
|
+
headers.delete(Headers::COOKIE)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
headers.delete(Headers::CONTENT_TYPE) if verb == :get
|
|
308
|
+
|
|
309
|
+
headers
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# @!attribute [r] host
|
|
313
|
+
# Host from the URI
|
|
314
|
+
# @return [String]
|
|
315
|
+
# @api private
|
|
316
|
+
def_delegator :@uri, :host
|
|
317
|
+
|
|
318
|
+
# Return the port for the request URI
|
|
319
|
+
# @return [Fixnum]
|
|
320
|
+
# @api private
|
|
321
|
+
def port
|
|
322
|
+
@uri.port || @uri.default_port
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Build default Host header value
|
|
326
|
+
# @return [String]
|
|
327
|
+
# @api private
|
|
328
|
+
def default_host_header_value
|
|
329
|
+
value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
|
330
|
+
|
|
331
|
+
raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
|
|
332
|
+
|
|
333
|
+
value
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Parse and normalize the URI, setting scheme
|
|
337
|
+
# @return [void]
|
|
338
|
+
# @api private
|
|
339
|
+
def parse_uri!(uri)
|
|
340
|
+
raise ArgumentError, "uri is nil" if uri.nil?
|
|
341
|
+
raise ArgumentError, "uri is empty" if uri.is_a?(String) && uri.empty?
|
|
342
|
+
|
|
343
|
+
@uri = @uri_normalizer.call(uri)
|
|
344
|
+
@scheme = String(@uri.scheme).downcase.to_sym if @uri.scheme
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Validate HTTP method and URI scheme
|
|
348
|
+
# @return [void]
|
|
349
|
+
# @api private
|
|
350
|
+
def validate_method_and_scheme!
|
|
351
|
+
raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
|
352
|
+
raise(HTTP::URI::InvalidError, "invalid URI: #{@uri}") unless @scheme
|
|
353
|
+
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Coerce input into a Body object
|
|
357
|
+
# @return [HTTP::Request::Body]
|
|
358
|
+
# @api private
|
|
359
|
+
def prepare_body(body)
|
|
360
|
+
body.is_a?(Body) ? body : Body.new(body)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Build headers with default values
|
|
364
|
+
# @return [HTTP::Headers]
|
|
365
|
+
# @api private
|
|
366
|
+
def prepare_headers(headers)
|
|
367
|
+
headers = Headers.coerce(headers || {})
|
|
368
|
+
|
|
369
|
+
headers[Headers::HOST] ||= default_host_header_value
|
|
370
|
+
headers[Headers::USER_AGENT] ||= USER_AGENT
|
|
371
|
+
|
|
372
|
+
headers
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require "http/client"
|
|
5
|
+
|
|
6
|
+
module HTTP
|
|
7
|
+
class Response
|
|
8
|
+
# A streamable response body, also easily converted into a string
|
|
9
|
+
class Body
|
|
10
|
+
extend Forwardable
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
def_delegator :to_s, :empty?
|
|
14
|
+
|
|
15
|
+
# The connection object for the request
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# body.connection
|
|
19
|
+
#
|
|
20
|
+
# @return [HTTP::Connection]
|
|
21
|
+
# @api public
|
|
22
|
+
attr_reader :connection
|
|
23
|
+
|
|
24
|
+
# The encoding used for the body content
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# body.encoding # => Encoding::UTF_8
|
|
28
|
+
#
|
|
29
|
+
# @return [Encoding]
|
|
30
|
+
# @api public
|
|
31
|
+
attr_reader :encoding
|
|
32
|
+
|
|
33
|
+
# Create a new Body instance
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# Body.new(stream, encoding: Encoding::UTF_8)
|
|
37
|
+
#
|
|
38
|
+
# @param stream [#readpartial] the response stream
|
|
39
|
+
# @param encoding [Encoding] the encoding to use
|
|
40
|
+
# @return [Body]
|
|
41
|
+
# @api public
|
|
42
|
+
def initialize(stream, encoding: Encoding::BINARY)
|
|
43
|
+
@stream = stream
|
|
44
|
+
@connection = stream.respond_to?(:connection) ? stream.connection : stream
|
|
45
|
+
@streaming = nil
|
|
46
|
+
@contents = nil
|
|
47
|
+
@encoding = find_encoding(encoding)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Read a chunk of the body
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# body.readpartial # => "chunk of data"
|
|
54
|
+
#
|
|
55
|
+
# (see HTTP::Client#readpartial)
|
|
56
|
+
# @return [String]
|
|
57
|
+
# @raise [EOFError] when no more data left
|
|
58
|
+
# @api public
|
|
59
|
+
def readpartial(*)
|
|
60
|
+
stream!
|
|
61
|
+
chunk = @stream.readpartial(*)
|
|
62
|
+
String.new(chunk, encoding: @encoding)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Iterate over the body, allowing it to be enumerable
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# body.each { |chunk| puts chunk }
|
|
69
|
+
#
|
|
70
|
+
# @yield [chunk] Passes each chunk to the block
|
|
71
|
+
# @yieldparam chunk [String]
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @api public
|
|
74
|
+
def each
|
|
75
|
+
loop do
|
|
76
|
+
yield readpartial
|
|
77
|
+
end
|
|
78
|
+
rescue EOFError # rubocop:disable Lint/SuppressedException
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Eagerly consume the entire body as a string
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# body.to_s # => "full response body"
|
|
85
|
+
#
|
|
86
|
+
# @return [String]
|
|
87
|
+
# @api public
|
|
88
|
+
def to_s
|
|
89
|
+
return @contents if @contents
|
|
90
|
+
|
|
91
|
+
raise StateError, "body is being streamed" unless @streaming.nil?
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
@streaming = false
|
|
95
|
+
@contents = read_contents
|
|
96
|
+
rescue
|
|
97
|
+
@contents = nil
|
|
98
|
+
raise
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
@contents
|
|
102
|
+
end
|
|
103
|
+
alias to_str to_s
|
|
104
|
+
|
|
105
|
+
# Assert that the body is actively being streamed
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# body.stream!
|
|
109
|
+
#
|
|
110
|
+
# @return [true]
|
|
111
|
+
# @api public
|
|
112
|
+
def stream!
|
|
113
|
+
raise StateError, "body has already been consumed" if @streaming.eql?(false)
|
|
114
|
+
|
|
115
|
+
@streaming = true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Whether the body content is suitable for logging
|
|
119
|
+
#
|
|
120
|
+
# Returns true when the body encoding is not binary. Binary responses
|
|
121
|
+
# (images, audio, compressed data) produce unreadable log output.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# body.loggable? # => true
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
# @api public
|
|
128
|
+
def loggable?
|
|
129
|
+
@encoding != Encoding::BINARY
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Easier to interpret string inspect
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# body.inspect # => "#<HTTP::Response::Body:3ff2 @streaming=false>"
|
|
136
|
+
#
|
|
137
|
+
# @return [String]
|
|
138
|
+
# @api public
|
|
139
|
+
def inspect
|
|
140
|
+
"#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Read all chunks into a single string
|
|
146
|
+
#
|
|
147
|
+
# @return [String]
|
|
148
|
+
# @api private
|
|
149
|
+
def read_contents
|
|
150
|
+
contents = String.new("", encoding: @encoding)
|
|
151
|
+
|
|
152
|
+
loop do
|
|
153
|
+
contents << String.new(@stream.readpartial, encoding: @encoding)
|
|
154
|
+
rescue EOFError
|
|
155
|
+
break
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
contents
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Retrieve encoding by name
|
|
162
|
+
#
|
|
163
|
+
# @return [Encoding]
|
|
164
|
+
# @api private
|
|
165
|
+
def find_encoding(encoding)
|
|
166
|
+
Encoding.find encoding
|
|
167
|
+
rescue ArgumentError
|
|
168
|
+
Encoding::BINARY
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module HTTP
|
|
6
|
+
class Response
|
|
7
|
+
# Decompresses gzip/deflate response body streams
|
|
8
|
+
class Inflater
|
|
9
|
+
# The underlying connection
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# inflater.connection
|
|
13
|
+
#
|
|
14
|
+
# @return [HTTP::Connection] the underlying connection
|
|
15
|
+
# @api public
|
|
16
|
+
attr_reader :connection
|
|
17
|
+
|
|
18
|
+
# Create a new Inflater wrapping a connection
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# Inflater.new(connection)
|
|
22
|
+
#
|
|
23
|
+
# @param connection [HTTP::Connection] the connection to inflate
|
|
24
|
+
# @return [Inflater]
|
|
25
|
+
# @api public
|
|
26
|
+
def initialize(connection)
|
|
27
|
+
@connection = connection
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Read and inflate a chunk of the response body
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# inflater.readpartial # => "decompressed data"
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
# @raise [EOFError] when no more data left
|
|
37
|
+
# @api public
|
|
38
|
+
def readpartial(*)
|
|
39
|
+
chunk = @connection.readpartial(*)
|
|
40
|
+
zstream.inflate(chunk)
|
|
41
|
+
rescue EOFError
|
|
42
|
+
unless zstream.closed?
|
|
43
|
+
zstream.finished? ? zstream.finish : zstream.reset
|
|
44
|
+
zstream.close
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
raise
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Return the zlib inflate stream
|
|
53
|
+
# @return [Zlib::Inflate]
|
|
54
|
+
# @api private
|
|
55
|
+
def zstream
|
|
56
|
+
@zstream ||= Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|