httpx 1.7.7 → 1.8.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/doc/release_notes/1_7_8.md +5 -0
- data/doc/release_notes/1_8_0.md +100 -0
- data/lib/httpx/adapters/datadog.rb +3 -1
- data/lib/httpx/connection/http1.rb +10 -1
- data/lib/httpx/connection/http2.rb +37 -4
- data/lib/httpx/connection.rb +76 -7
- data/lib/httpx/errors.rb +8 -1
- data/lib/httpx/io/tcp.rb +11 -1
- data/lib/httpx/options.rb +16 -4
- data/lib/httpx/parser/http1.rb +8 -2
- data/lib/httpx/plugins/auth.rb +52 -4
- data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
- data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
- data/lib/httpx/plugins/cache.rb +221 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +50 -3
- data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
- data/lib/httpx/plugins/oauth.rb +66 -14
- data/lib/httpx/plugins/proxy.rb +5 -0
- data/lib/httpx/plugins/response_cache.rb +26 -105
- data/lib/httpx/plugins/retries.rb +13 -5
- data/lib/httpx/plugins/server_sent_events.rb +158 -0
- data/lib/httpx/plugins/ssrf_filter.rb +16 -1
- data/lib/httpx/plugins/stream.rb +7 -3
- data/lib/httpx/plugins/tracing.rb +15 -4
- data/lib/httpx/request.rb +18 -1
- data/lib/httpx/resolver/cache/file.rb +56 -0
- data/lib/httpx/resolver/native.rb +14 -3
- data/lib/httpx/response/body.rb +4 -2
- data/lib/httpx/response.rb +9 -1
- data/lib/httpx/selector.rb +7 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +3 -0
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +1 -1
- data/sig/connection.rbs +11 -8
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +2 -0
- data/sig/io/tcp.rbs +2 -0
- data/sig/loggable.rbs +4 -0
- data/sig/options.rbs +25 -12
- data/sig/parser/http1.rbs +3 -1
- data/sig/plugins/auth/ntlm.rbs +1 -1
- data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
- data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
- data/sig/plugins/cache.rbs +69 -0
- data/sig/plugins/fiber_concurrency.rbs +4 -0
- data/sig/plugins/ntlm_v2_auth.rbs +36 -0
- data/sig/plugins/response_cache.rbs +13 -38
- data/sig/plugins/retries.rbs +5 -5
- data/sig/plugins/server_sent_events.rbs +45 -0
- data/sig/plugins/ssrf_filter.rbs +5 -1
- data/sig/plugins/stream.rbs +1 -1
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/webdav.rbs +1 -1
- data/sig/pool.rbs +2 -2
- data/sig/request.rbs +7 -3
- data/sig/resolver/cache/file.rbs +13 -0
- data/sig/resolver/entry.rbs +1 -1
- data/sig/resolver/https.rbs +3 -3
- data/sig/resolver/multi.rbs +1 -1
- data/sig/resolver/native.rbs +5 -5
- data/sig/resolver/resolver.rbs +1 -3
- data/sig/resolver/system.rbs +2 -2
- data/sig/resolver.rbs +3 -0
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +11 -8
- data/sig/timers.rbs +5 -5
- data/sig/transcoder/body.rbs +1 -1
- data/sig/transcoder/gzip.rbs +3 -2
- data/sig/transcoder/multipart.rbs +4 -1
- data/sig/transcoder/utils/deflater.rbs +2 -0
- data/sig/transcoder.rbs +2 -0
- data/sig/utils.rbs +1 -1
- metadata +19 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74104ce6c4d05053e2a005974de5f0cd66346a1cb4558b7c0887d3a2d691d226
|
|
4
|
+
data.tar.gz: ca0ce3e30f509d44e4c0daac30a0b5d9949c118c19a715561c276f1debaf5dbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0de3c6baf56c86272cc45bdc06e1187655e759ddf329414d71fcbf4603c9d0e03791641e05f5c07ef84cd578a3bba1a855fde77b5b0f1f2e11df1cf60ea0815a
|
|
7
|
+
data.tar.gz: a276c6ed937c8d04103382d517d56d9df805886f23b05a38f6e654fcd6935e17407101e1eedc2b6605b4f347a37262116024a61633ebdd660131b06fc1f3fde9
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 1.8.0
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
### New plugins
|
|
6
|
+
|
|
7
|
+
#### `:server_sent_events` plugin
|
|
8
|
+
|
|
9
|
+
The `:server_sent_events` plugin provides a convenience API to deal with `text/event-stream` requests, on top of the `:stream` plugin.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
session = HTTPX.plugin(:server_sent_events)
|
|
13
|
+
|
|
14
|
+
sse_response = session.get("https://example.com/event-stream", event_stream: true)
|
|
15
|
+
|
|
16
|
+
sse_response.each_message do |message|
|
|
17
|
+
puts message.id
|
|
18
|
+
puts message.event
|
|
19
|
+
puts message.data
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You can read more about it in https://gitlab.com/os85/httpx/wikis/Server-Sent-Events .
|
|
24
|
+
|
|
25
|
+
#### `:cache` plugin
|
|
26
|
+
|
|
27
|
+
The `:cache` plugin allows caching responses. It exposes some options to determine some of its functionality, i.e. whether a request can use a cached response, whether a response can be cached, whether a cached response is still valid, etc.
|
|
28
|
+
|
|
29
|
+
This functionality was extracted from the `:response_cache` plugin, which now uses it under the hood.
|
|
30
|
+
|
|
31
|
+
You can read more about it in https://gitlab.com/os85/httpx/wikis/Cache .
|
|
32
|
+
|
|
33
|
+
#### `:ntlm_v2_auth` plugin
|
|
34
|
+
|
|
35
|
+
The `:ntlm_v2_auth` plugin is now available. It implements the most recent version of the NTLM authentication scheme supported by Microsoft products.
|
|
36
|
+
|
|
37
|
+
You can read more about it in https://gitlab.com/os85/httpx/wikis/Auth#ntlm-v2-auth .
|
|
38
|
+
|
|
39
|
+
### New timeouts
|
|
40
|
+
|
|
41
|
+
#### `:total_request_timeout`
|
|
42
|
+
|
|
43
|
+
You can use the `:total_request_timeout` to time the time it takes a request to get its final response. This includes when your requests follows redirects (via the `:follow_redirects` plugin) or is retried multiple times (via the `:retries´ plugin).
|
|
44
|
+
|
|
45
|
+
#### `:ping_timeout`
|
|
46
|
+
|
|
47
|
+
Defines the number of seconds a connection waits to receive a ping response when probing for a connection for liveness.
|
|
48
|
+
|
|
49
|
+
Defaults to 2 seconds.
|
|
50
|
+
|
|
51
|
+
### `:ssrf_filter` plugin new options
|
|
52
|
+
|
|
53
|
+
#### `:extra_unsafe_ranges`
|
|
54
|
+
|
|
55
|
+
A list of extra unsafe IPs or IP ranges to the default deny list.
|
|
56
|
+
|
|
57
|
+
#### `:safe_private_ranges`
|
|
58
|
+
|
|
59
|
+
A list of IPs or IP ranges which are allowed and would otherwise be denied.
|
|
60
|
+
|
|
61
|
+
### `:auth` plugin new option
|
|
62
|
+
|
|
63
|
+
#### `:reset_auth_header_expires_in/at`
|
|
64
|
+
|
|
65
|
+
The `:reset_auth_header_expires_in` and `:reset_auth_header_expires_at` options enable discarding an authorization token an X number of seconds after it has been generated, or at a particuar point in time, respectively. This is useful when the token is dynamically generated, so that you can preemptively renegotiate a new one.
|
|
66
|
+
|
|
67
|
+
The `:oauth` plugin makes use of these fields, alongside the `expires_in` claim from token responses, to refresh the token as soon as the it expires.
|
|
68
|
+
|
|
69
|
+
### `:max_response_body_size`
|
|
70
|
+
|
|
71
|
+
Can be set to the maximum number of bytes a response may have, after which it'll return an `HTTPX::ErrorResponse`. The limit is enforced based on the `content-length` header and as bytes are received.
|
|
72
|
+
|
|
73
|
+
### `:max_response_headers`
|
|
74
|
+
|
|
75
|
+
Can be set to the maximum number of headers a response may have.
|
|
76
|
+
|
|
77
|
+
### `:max_response_header_value_size`
|
|
78
|
+
|
|
79
|
+
Can be set to the maximum number of bytes a header value may have. In cases where a header field may be spread across multiple entries (ex. `"cookie"`), the limit is enforced on the aggregate byte size.
|
|
80
|
+
|
|
81
|
+
### resolver file cache
|
|
82
|
+
|
|
83
|
+
By specifying `:file` as the resolver `:cache` option, the DNS entries will be cached in a known location in your file system. This will allow sharing entries across processes within the same machine to reduce overall DNS traffic. You can use it via:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
session = HTTPX.with(resolver_options: { cache: :file })
|
|
87
|
+
session.get("https://example.com")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Improvements
|
|
91
|
+
|
|
92
|
+
* `http-2` minimum version is now 1.2.0, which brings performance benefits around frame parsing and other common operations.
|
|
93
|
+
|
|
94
|
+
## Bugfixes
|
|
95
|
+
|
|
96
|
+
* several fixes to make `httpx` usable inside a fiber scheduler in ruby 4.
|
|
97
|
+
* `:proxy` plugin: unescape user/password from proxy options before reusing it (to avoid using percent-encoded values when p.ex. generating base64-encoding for basic auth).
|
|
98
|
+
* `:retries` plugin: fixed the polynomial and exponential backoff `:retry_after` strategies calculation.
|
|
99
|
+
* `:tracing` plugin: fixing span start time set up when request is sent to a closed connection, or when a long-lived connection will be probed for liveness.
|
|
100
|
+
* datadog adapter: fixed integration with the `datadog` gem v2.34 or higher.
|
|
@@ -15,8 +15,10 @@ module Datadog::Tracing
|
|
|
15
15
|
|
|
16
16
|
TAG_BASE_SERVICE = if Gem::Version.new(DATADOG_VERSION::STRING) < Gem::Version.new("1.15.0")
|
|
17
17
|
"_dd.base_service"
|
|
18
|
-
|
|
18
|
+
elsif Gem::Version.new(DATADOG_VERSION::STRING) < Gem::Version.new("2.34.0")
|
|
19
19
|
Datadog::Tracing::Contrib::Ext::Metadata::TAG_BASE_SERVICE
|
|
20
|
+
else
|
|
21
|
+
Datadog::Tracing::Metadata::Ext::TAG_BASE_SERVICE
|
|
20
22
|
end
|
|
21
23
|
TAG_PEER_HOSTNAME = Datadog::Tracing::Metadata::Ext::TAG_PEER_HOSTNAME
|
|
22
24
|
TAG_PEER_SERVICE = Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE
|
|
@@ -14,6 +14,7 @@ module HTTPX
|
|
|
14
14
|
"www-authenticate" => "WWW-Authenticate",
|
|
15
15
|
"http2-settings" => "HTTP2-Settings",
|
|
16
16
|
"content-md5" => "Content-MD5",
|
|
17
|
+
"last-event-id" => "Last-Event-ID",
|
|
17
18
|
}.freeze
|
|
18
19
|
attr_reader :pending, :requests
|
|
19
20
|
|
|
@@ -23,7 +24,7 @@ module HTTPX
|
|
|
23
24
|
@options = options
|
|
24
25
|
@max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
|
|
25
26
|
@max_requests = @options.max_requests
|
|
26
|
-
@parser = Parser::HTTP1.new(self)
|
|
27
|
+
@parser = Parser::HTTP1.new(self, options.max_response_headers, options.max_response_header_value_size)
|
|
27
28
|
@buffer = buffer
|
|
28
29
|
@version = [1, 1]
|
|
29
30
|
@pending = []
|
|
@@ -47,6 +48,10 @@ module HTTPX
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def reset
|
|
51
|
+
if @ping_timer
|
|
52
|
+
@ping_timer.cancel
|
|
53
|
+
@ping_timer = nil
|
|
54
|
+
end
|
|
50
55
|
@max_requests = @options.max_requests || MAX_REQUESTS
|
|
51
56
|
@parser.reset!
|
|
52
57
|
@handshake_completed = false
|
|
@@ -132,6 +137,10 @@ module HTTPX
|
|
|
132
137
|
request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
|
|
133
138
|
request.log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
|
|
134
139
|
|
|
140
|
+
if response.content_length && response.content_length > request.options.max_response_body_size
|
|
141
|
+
raise HTTPX::Error, "maximum response body size exceeded"
|
|
142
|
+
end
|
|
143
|
+
|
|
135
144
|
request.response = response
|
|
136
145
|
on_complete if response.finished?
|
|
137
146
|
end
|
|
@@ -118,7 +118,7 @@ module HTTPX
|
|
|
118
118
|
return false
|
|
119
119
|
end
|
|
120
120
|
unless (stream = @streams[request])
|
|
121
|
-
stream = @connection.new_stream
|
|
121
|
+
stream = @connection.new_stream(**request.http2_stream_options)
|
|
122
122
|
handle_stream(stream, request)
|
|
123
123
|
@streams[request] = stream
|
|
124
124
|
@max_requests -= 1
|
|
@@ -128,6 +128,8 @@ module HTTPX
|
|
|
128
128
|
rescue ::HTTP2::Error::StreamLimitExceeded
|
|
129
129
|
@pending.unshift(request)
|
|
130
130
|
false
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
emit(:error, request, e)
|
|
131
133
|
end
|
|
132
134
|
|
|
133
135
|
def consume
|
|
@@ -312,7 +314,21 @@ module HTTPX
|
|
|
312
314
|
end
|
|
313
315
|
_, status = h.shift
|
|
314
316
|
headers = request.options.headers_class.new(h)
|
|
317
|
+
|
|
318
|
+
raise HTTPX::Error, "maximum number of response headers exceeded" if h.size > @options.max_response_headers
|
|
319
|
+
|
|
320
|
+
if (max_header_value_size = @options.max_response_header_value_size)
|
|
321
|
+
headers.each do |_, v| # rubocop:disable Style/HashEachMethods
|
|
322
|
+
raise HTTPX::Error, "maximum header value size exceeded" if v.size > max_header_value_size
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
315
326
|
response = request.options.response_class.new(request, status, "2.0", headers)
|
|
327
|
+
|
|
328
|
+
if response.content_length && response.content_length > request.options.max_response_body_size
|
|
329
|
+
raise HTTPX::Error.new, "maximum response body size exceeded"
|
|
330
|
+
end
|
|
331
|
+
|
|
316
332
|
request.response = response
|
|
317
333
|
@streams[request] = stream
|
|
318
334
|
|
|
@@ -428,11 +444,28 @@ module HTTPX
|
|
|
428
444
|
end
|
|
429
445
|
|
|
430
446
|
def frame_with_extra_info(frame)
|
|
447
|
+
flags_bits = frame.fetch(:flags, 0)
|
|
431
448
|
case frame[:type]
|
|
432
449
|
when :data
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
450
|
+
flags = [] #: Array[Symbol]
|
|
451
|
+
flags << :end_stream if flags_bits.anybits?(0b0001)
|
|
452
|
+
flags << :padded if flags_bits.anybits?(0b1000)
|
|
453
|
+
frame.merge(payload: frame[:payload].bytesize, flags: flags)
|
|
454
|
+
when :push_promise, :headers
|
|
455
|
+
flags = [] #: Array[Symbol]
|
|
456
|
+
flags << :end_stream if flags_bits.anybits?(0b0001)
|
|
457
|
+
flags << :priority if flags_bits.anybits?(0b0010)
|
|
458
|
+
flags << :end_headers if flags_bits.anybits?(0b0100)
|
|
459
|
+
flags << :padded if flags_bits.anybits?(0b1000)
|
|
460
|
+
frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
|
|
461
|
+
when :ping
|
|
462
|
+
flags = [] #: Array[Symbol]
|
|
463
|
+
flags << :ack if flags_bits.anybits?(0b0001)
|
|
464
|
+
frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
|
|
465
|
+
when :settings
|
|
466
|
+
flags = [] #: Array[Symbol]
|
|
467
|
+
flags << :ack if flags_bits.anybits?(0b0001)
|
|
468
|
+
frame.merge(flags: flags)
|
|
436
469
|
when :window_update
|
|
437
470
|
connection_or_stream = if (id = frame[:stream]).zero?
|
|
438
471
|
@connection
|
data/lib/httpx/connection.rb
CHANGED
|
@@ -47,8 +47,8 @@ module HTTPX
|
|
|
47
47
|
def initialize(uri, options)
|
|
48
48
|
@current_session = @current_selector = @max_concurrent_requests =
|
|
49
49
|
@parser = @sibling = @coalesced_connection = @altsvc_connection =
|
|
50
|
-
@
|
|
51
|
-
|
|
50
|
+
@ping_timer = @family = @io = @ssl_session =
|
|
51
|
+
@timeout = @connected_at = @response_received_at = nil
|
|
52
52
|
|
|
53
53
|
@exhausted = @cloned = @main_sibling = false
|
|
54
54
|
|
|
@@ -222,10 +222,27 @@ module HTTPX
|
|
|
222
222
|
|
|
223
223
|
consume
|
|
224
224
|
when :closed
|
|
225
|
-
return
|
|
225
|
+
return if @pending.empty?
|
|
226
|
+
|
|
227
|
+
# there are pending requests to send, restart the state machine.
|
|
228
|
+
idling
|
|
229
|
+
|
|
230
|
+
# @fiber-switch-guard
|
|
231
|
+
# fiber may have switch after ensuring that @io is closed.
|
|
232
|
+
return unless @state == :idle
|
|
233
|
+
|
|
234
|
+
call
|
|
226
235
|
when :closing
|
|
227
236
|
consume
|
|
228
237
|
transition(:closed)
|
|
238
|
+
|
|
239
|
+
# @fiber-switch-guard
|
|
240
|
+
# fiber may have switch while closing @io.
|
|
241
|
+
return if @state == :closed &&
|
|
242
|
+
# only remain here if there are pending requests.
|
|
243
|
+
@pending.empty?
|
|
244
|
+
|
|
245
|
+
call
|
|
229
246
|
when :open
|
|
230
247
|
consume
|
|
231
248
|
end
|
|
@@ -251,7 +268,12 @@ module HTTPX
|
|
|
251
268
|
case @state
|
|
252
269
|
when :idle
|
|
253
270
|
purge_after_closed
|
|
254
|
-
|
|
271
|
+
|
|
272
|
+
# @fiber-switch-guard
|
|
273
|
+
if @io.can_disconnect? && @pending.empty?
|
|
274
|
+
disconnect
|
|
275
|
+
return
|
|
276
|
+
end
|
|
255
277
|
when :closed
|
|
256
278
|
@connected_at = nil
|
|
257
279
|
end
|
|
@@ -319,7 +341,7 @@ module HTTPX
|
|
|
319
341
|
@pending << request
|
|
320
342
|
transition(:active) if @state == :inactive
|
|
321
343
|
request.ping!
|
|
322
|
-
ping
|
|
344
|
+
ping(request)
|
|
323
345
|
return
|
|
324
346
|
end
|
|
325
347
|
|
|
@@ -341,6 +363,9 @@ module HTTPX
|
|
|
341
363
|
|
|
342
364
|
def idling
|
|
343
365
|
purge_after_closed
|
|
366
|
+
|
|
367
|
+
return unless @state == :closed
|
|
368
|
+
|
|
344
369
|
@write_buffer.clear
|
|
345
370
|
transition(:idle)
|
|
346
371
|
return unless @parser
|
|
@@ -421,6 +446,8 @@ module HTTPX
|
|
|
421
446
|
if @timeout
|
|
422
447
|
@timeout -= error.timeout
|
|
423
448
|
return unless @timeout <= 0
|
|
449
|
+
|
|
450
|
+
@timeout = nil
|
|
424
451
|
end
|
|
425
452
|
|
|
426
453
|
error = error.to_connection_error if connecting?
|
|
@@ -660,7 +687,12 @@ module HTTPX
|
|
|
660
687
|
@exhausted = true
|
|
661
688
|
parser.close
|
|
662
689
|
|
|
690
|
+
# @fiber-switch-guard
|
|
691
|
+
# fiber may have switched while closing @io, check whether still in the exhausted loop.
|
|
692
|
+
next unless @exhausted
|
|
693
|
+
|
|
663
694
|
idling
|
|
695
|
+
|
|
664
696
|
@exhausted = false
|
|
665
697
|
end
|
|
666
698
|
parser.on(:origin) do |origin|
|
|
@@ -676,6 +708,9 @@ module HTTPX
|
|
|
676
708
|
enqueue_pending_requests_from_parser(parser)
|
|
677
709
|
|
|
678
710
|
reset
|
|
711
|
+
|
|
712
|
+
next unless @state == :closed
|
|
713
|
+
|
|
679
714
|
# :reset event only fired in http/1.1, so this guarantees
|
|
680
715
|
# that the connection will be closed here.
|
|
681
716
|
idling unless @pending.empty?
|
|
@@ -766,6 +801,9 @@ module HTTPX
|
|
|
766
801
|
return unless @write_buffer.empty?
|
|
767
802
|
|
|
768
803
|
purge_after_closed
|
|
804
|
+
|
|
805
|
+
# @fiber-switch-guard
|
|
806
|
+
return unless @state == :closing && (@io.nil? || @io.can_disconnect?)
|
|
769
807
|
when :already_open
|
|
770
808
|
nextstate = :open
|
|
771
809
|
# the first check for given io readiness must still use a timeout.
|
|
@@ -833,7 +871,15 @@ module HTTPX
|
|
|
833
871
|
end
|
|
834
872
|
|
|
835
873
|
def purge_after_closed
|
|
836
|
-
|
|
874
|
+
if @io
|
|
875
|
+
@io.close
|
|
876
|
+
|
|
877
|
+
# @fiber-switch-guard
|
|
878
|
+
# due to fiber scheduler, multiple fibers may be listening on the same connection
|
|
879
|
+
# and moving the state machine forward; in such cases, when the control flow reaches
|
|
880
|
+
# this line, the io object may not be closed anymore.
|
|
881
|
+
return unless @io&.can_disconnect?
|
|
882
|
+
end
|
|
837
883
|
@read_buffer.clear
|
|
838
884
|
@timeout = nil
|
|
839
885
|
end
|
|
@@ -906,14 +952,24 @@ module HTTPX
|
|
|
906
952
|
end
|
|
907
953
|
end
|
|
908
954
|
|
|
909
|
-
def ping
|
|
955
|
+
def ping(_request)
|
|
910
956
|
return if parser.waiting_for_ping?
|
|
911
957
|
|
|
912
958
|
parser.ping
|
|
959
|
+
|
|
960
|
+
ping_timeout = @options.timeout[:ping_timeout]
|
|
961
|
+
|
|
962
|
+
@ping_timer = @current_selector.after(ping_timeout) do
|
|
963
|
+
error = PingTimeoutError.new(ping_timeout, "Timed out after #{ping_timeout} seconds")
|
|
964
|
+
on_error(error)
|
|
965
|
+
end
|
|
966
|
+
|
|
913
967
|
call
|
|
914
968
|
end
|
|
915
969
|
|
|
916
970
|
def pong
|
|
971
|
+
@ping_timer.cancel
|
|
972
|
+
@ping_timer = nil
|
|
917
973
|
@response_received_at = Utils.now
|
|
918
974
|
@no_more_requests_counter = 0
|
|
919
975
|
send_pending
|
|
@@ -964,6 +1020,7 @@ module HTTPX
|
|
|
964
1020
|
set_request_write_timeout(request)
|
|
965
1021
|
set_request_read_timeout(request)
|
|
966
1022
|
set_request_request_timeout(request)
|
|
1023
|
+
set_request_total_request_timeout(request)
|
|
967
1024
|
end
|
|
968
1025
|
|
|
969
1026
|
def set_request_read_timeout(request)
|
|
@@ -1016,6 +1073,18 @@ module HTTPX
|
|
|
1016
1073
|
request.handle_error(error)
|
|
1017
1074
|
end
|
|
1018
1075
|
|
|
1076
|
+
def set_request_total_request_timeout(request)
|
|
1077
|
+
return if request.started?
|
|
1078
|
+
|
|
1079
|
+
total_request_timeout = request.total_request_timeout
|
|
1080
|
+
|
|
1081
|
+
return if total_request_timeout.nil? || total_request_timeout.infinite?
|
|
1082
|
+
|
|
1083
|
+
set_request_timeout(:total_request_timeout, request, total_request_timeout, :headers, :complete) do
|
|
1084
|
+
read_timeout_callback(request, total_request_timeout, TotalRequestTimeoutError)
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1019
1088
|
def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
|
|
1020
1089
|
request.set_timeout_callback(start_event) do
|
|
1021
1090
|
unless (selector = @current_selector)
|
data/lib/httpx/errors.rb
CHANGED
|
@@ -62,15 +62,22 @@ module HTTPX
|
|
|
62
62
|
# Error raised when there was a timeout while sending a request from the server.
|
|
63
63
|
class WriteTimeoutError < RequestTimeoutError; end
|
|
64
64
|
|
|
65
|
+
# Error raised when a response couldn't be received for a request after multiple interactions.
|
|
66
|
+
# This error should not be retriable.
|
|
67
|
+
class TotalRequestTimeoutError < RequestTimeoutError; end
|
|
68
|
+
|
|
65
69
|
# Error raised when there was a timeout while waiting for the HTTP/2 settings frame from the server.
|
|
66
70
|
class SettingsTimeoutError < TimeoutError; end
|
|
67
71
|
|
|
68
72
|
# Error raised when there was a timeout while resolving a domain to an IP.
|
|
69
73
|
class ResolveTimeoutError < TimeoutError; end
|
|
70
74
|
|
|
71
|
-
# Error
|
|
75
|
+
# Error raised when there was a timeout waiting for readiness of the socket the request is related to.
|
|
72
76
|
class OperationTimeoutError < TimeoutError; end
|
|
73
77
|
|
|
78
|
+
# Error raised when a connection liveness probe (aka ping) times out.
|
|
79
|
+
class PingTimeoutError < TimeoutError; end
|
|
80
|
+
|
|
74
81
|
# Error raised when there was an error while resolving a domain to an IP.
|
|
75
82
|
class ResolveError < Error; end
|
|
76
83
|
|
data/lib/httpx/io/tcp.rb
CHANGED
|
@@ -190,10 +190,20 @@ module HTTPX
|
|
|
190
190
|
log { "error closing socket" }
|
|
191
191
|
log { e.full_message(highlight: false) }
|
|
192
192
|
ensure
|
|
193
|
-
|
|
193
|
+
# @fiber-switch-guard
|
|
194
|
+
# ensure that all :closed IOs don't leave dangling sockets
|
|
195
|
+
# behind. This may happen in a fiber scheduler scenario where
|
|
196
|
+
# connection is reused across fibers.
|
|
197
|
+
transition(:closed) if @io.closed?
|
|
194
198
|
end
|
|
195
199
|
end
|
|
196
200
|
|
|
201
|
+
# signals that the connection that contains this IO can be checked back into the pool.
|
|
202
|
+
# that includes sockets opened outside of the scope of the session, or closed IOs.
|
|
203
|
+
def can_disconnect?
|
|
204
|
+
@keep_open || @state == :closed
|
|
205
|
+
end
|
|
206
|
+
|
|
197
207
|
def connected?
|
|
198
208
|
@state == :connected
|
|
199
209
|
end
|
data/lib/httpx/options.rb
CHANGED
|
@@ -8,12 +8,12 @@ module HTTPX
|
|
|
8
8
|
WINDOW_SIZE = 1 << 14 # 16K
|
|
9
9
|
MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
|
|
10
10
|
KEEP_ALIVE_TIMEOUT = 20
|
|
11
|
+
PING_TIMEOUT = 2
|
|
11
12
|
SETTINGS_TIMEOUT = 10
|
|
12
13
|
CLOSE_HANDSHAKE_TIMEOUT = 10
|
|
13
14
|
CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
|
|
14
|
-
REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
|
|
15
|
+
REQUEST_TIMEOUT = OPERATION_TIMEOUT = TOTAL_REQUEST_TIMEOUT = nil
|
|
15
16
|
RESOLVER_TYPES = %i[memory file].freeze
|
|
16
|
-
|
|
17
17
|
# default value used for "user-agent" header, when not overridden.
|
|
18
18
|
USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
|
|
19
19
|
|
|
@@ -79,9 +79,15 @@ module HTTPX
|
|
|
79
79
|
# :decompress_response_body :: whether to auto-decompress response body (defaults to <tt>true</tt>).
|
|
80
80
|
# :compress_request_body :: whether to auto-decompress response body (defaults to <tt>true</tt>)
|
|
81
81
|
# :timeout :: hash of timeout configurations (supports <tt>:connect_timeout</tt>, <tt>:settings_timeout</tt>,
|
|
82
|
-
# <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>,
|
|
83
|
-
# and <tt>:
|
|
82
|
+
# <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>, <tt>:read_timeout</tt>, <tt>:write_timeout</tt>,
|
|
83
|
+
# <tt>:request_timeout</tt>, <tt>:total_request_timeout</tt> and <tt>:ping_timeout</tt>,
|
|
84
84
|
# :headers :: hash of HTTP headers (ex: <tt>{ "x-custom-foo" => "bar" }</tt>)
|
|
85
|
+
# :max_response_body_size :: maximum size (in bytes) that the response body can consume (no threshold by default), after which an
|
|
86
|
+
# error is raised.
|
|
87
|
+
# :max_response_headers :: maximum number of header fields that a response can receive, after which an error is raised.
|
|
88
|
+
# :max_response_header_value_size :: maximum size (in bytes) a header value can have (no threshold by default).
|
|
89
|
+
# for cases where the value is broken into multiple header fields (such as "cookie" or "set-cookie"),
|
|
90
|
+
# this is the total aggregated size.
|
|
85
91
|
# :window_size :: number of bytes to read from a socket
|
|
86
92
|
# :buffer_size :: internal read and write buffer size in bytes
|
|
87
93
|
# :body_threshold_size :: maximum size in bytes of response payload that is buffered in memory.
|
|
@@ -387,6 +393,7 @@ module HTTPX
|
|
|
387
393
|
# number options
|
|
388
394
|
%i[
|
|
389
395
|
max_concurrent_requests max_requests window_size buffer_size
|
|
396
|
+
max_response_body_size max_response_headers max_response_header_value_size
|
|
390
397
|
body_threshold_size debug_level
|
|
391
398
|
].each do |option|
|
|
392
399
|
class_eval(<<-OUT, __FILE__, __LINE__ + 1)
|
|
@@ -553,15 +560,20 @@ module HTTPX
|
|
|
553
560
|
:supported_compression_formats => %w[gzip deflate],
|
|
554
561
|
:decompress_response_body => true,
|
|
555
562
|
:compress_request_body => true,
|
|
563
|
+
:max_response_headers => 1000,
|
|
564
|
+
:max_response_header_value_size => nil,
|
|
565
|
+
:max_response_body_size => Float::INFINITY,
|
|
556
566
|
:timeout => {
|
|
557
567
|
connect_timeout: CONNECT_TIMEOUT,
|
|
558
568
|
settings_timeout: SETTINGS_TIMEOUT,
|
|
559
569
|
close_handshake_timeout: CLOSE_HANDSHAKE_TIMEOUT,
|
|
560
570
|
operation_timeout: OPERATION_TIMEOUT,
|
|
561
571
|
keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
|
|
572
|
+
ping_timeout: PING_TIMEOUT,
|
|
562
573
|
read_timeout: READ_TIMEOUT,
|
|
563
574
|
write_timeout: WRITE_TIMEOUT,
|
|
564
575
|
request_timeout: REQUEST_TIMEOUT,
|
|
576
|
+
total_request_timeout: TOTAL_REQUEST_TIMEOUT,
|
|
565
577
|
}.freeze,
|
|
566
578
|
:headers_class => Class.new(Headers, &SET_TEMPORARY_NAME),
|
|
567
579
|
:headers => EMPTY_HASH,
|
data/lib/httpx/parser/http1.rb
CHANGED
|
@@ -9,11 +9,13 @@ module HTTPX
|
|
|
9
9
|
|
|
10
10
|
attr_reader :status_code, :http_version, :headers
|
|
11
11
|
|
|
12
|
-
def initialize(observer)
|
|
12
|
+
def initialize(observer, max_headers, max_header_value_size)
|
|
13
13
|
@observer = observer
|
|
14
14
|
@state = :idle
|
|
15
15
|
@buffer = "".b
|
|
16
16
|
@headers = {}
|
|
17
|
+
@max_headers = max_headers
|
|
18
|
+
@max_header_value_size = max_header_value_size
|
|
17
19
|
@content_length = nil
|
|
18
20
|
@_has_trailers = @upgrade = false
|
|
19
21
|
end
|
|
@@ -117,7 +119,11 @@ module HTTPX
|
|
|
117
119
|
value.strip!
|
|
118
120
|
raise Error, "wrong header format" if value.nil?
|
|
119
121
|
|
|
120
|
-
(headers[key.downcase] ||= []) << value
|
|
122
|
+
values = (headers[key.downcase] ||= []) << value
|
|
123
|
+
|
|
124
|
+
raise Error, "maximum header value size exceeded" if @max_header_value_size && (values.sum(&:size) > @max_header_value_size)
|
|
125
|
+
|
|
126
|
+
raise Error, "maximum number of response headers exceeded" if headers.size > @max_headers
|
|
121
127
|
end
|
|
122
128
|
end
|
|
123
129
|
|
data/lib/httpx/plugins/auth.rb
CHANGED
|
@@ -20,6 +20,9 @@ module HTTPX
|
|
|
20
20
|
#
|
|
21
21
|
# :auth_header_value :: the token to use as a string, or a callable which returns a string when called.
|
|
22
22
|
# :auth_header_type :: the authentication type to use in the "authorization" header value (i.e. "Bearer", "Digest"...)
|
|
23
|
+
# :auth_header_expires_at :: timestamp at which the auth header will be discarded. should be a callable (like a proc)
|
|
24
|
+
# receiving the request as an argument, and should return either a Time object, or an integer (UNIX time).
|
|
25
|
+
# :auth_header_expires_in :: time (in seconds) since the first use of an auth header after which that header will be discarded.
|
|
23
26
|
# :generate_auth_value_on_retry :: callable which returns whether the request should regenerate the auth_header_value
|
|
24
27
|
# when the request is retried (this option will only work if the session also loads the
|
|
25
28
|
# <tt>:retries</tt> plugin).
|
|
@@ -32,6 +35,22 @@ module HTTPX
|
|
|
32
35
|
value
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
def option_auth_header_expires_at(value)
|
|
39
|
+
unless value.respond_to?(:call)
|
|
40
|
+
value = Float(value)
|
|
41
|
+
raise TypeError, "`:auth_header_expires_at` must be positive" unless value.positive?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def option_auth_header_expires_in(value)
|
|
48
|
+
value = Float(value)
|
|
49
|
+
raise TypeError, "`:auth_header_expires_in` must be positive" unless value.positive?
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
35
54
|
def option_generate_auth_value_on_retry(value)
|
|
36
55
|
raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
|
|
37
56
|
|
|
@@ -43,7 +62,7 @@ module HTTPX
|
|
|
43
62
|
def initialize(*)
|
|
44
63
|
super
|
|
45
64
|
|
|
46
|
-
@auth_header_value = nil
|
|
65
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
47
66
|
@auth_header_value_mtx = Thread::Mutex.new
|
|
48
67
|
@skip_auth_header_value = false
|
|
49
68
|
end
|
|
@@ -65,7 +84,7 @@ module HTTPX
|
|
|
65
84
|
|
|
66
85
|
def reset_auth_header_value!
|
|
67
86
|
@auth_header_value_mtx.synchronize do
|
|
68
|
-
@auth_header_value = nil
|
|
87
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
69
88
|
end
|
|
70
89
|
end
|
|
71
90
|
|
|
@@ -75,7 +94,12 @@ module HTTPX
|
|
|
75
94
|
return super if @skip_auth_header_value || request.authorized?
|
|
76
95
|
|
|
77
96
|
auth_header_value = @auth_header_value_mtx.synchronize do
|
|
78
|
-
|
|
97
|
+
try_invalidate_auth_header_value
|
|
98
|
+
|
|
99
|
+
@auth_header_value ||= begin
|
|
100
|
+
set_auth_header_expires_at(request)
|
|
101
|
+
generate_auth_token
|
|
102
|
+
end
|
|
79
103
|
end
|
|
80
104
|
|
|
81
105
|
request.authorize(auth_header_value) if auth_header_value
|
|
@@ -83,6 +107,14 @@ module HTTPX
|
|
|
83
107
|
super
|
|
84
108
|
end
|
|
85
109
|
|
|
110
|
+
def try_invalidate_auth_header_value
|
|
111
|
+
return unless (expires_at = @auth_header_expires_at)
|
|
112
|
+
|
|
113
|
+
return if expires_at > Time.now.utc.to_i
|
|
114
|
+
|
|
115
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
116
|
+
end
|
|
117
|
+
|
|
86
118
|
def generate_auth_token
|
|
87
119
|
return unless (auth_value = @options.auth_header_value)
|
|
88
120
|
|
|
@@ -91,6 +123,19 @@ module HTTPX
|
|
|
91
123
|
auth_value
|
|
92
124
|
end
|
|
93
125
|
|
|
126
|
+
def set_auth_header_expires_at(request)
|
|
127
|
+
@auth_header_expires_at = if (expires_in = request.options.auth_header_expires_in)
|
|
128
|
+
Time.now.to_i + expires_in
|
|
129
|
+
elsif (expires_at = request.options.auth_header_expires_at)
|
|
130
|
+
if expires_at.respond_to?(:call)
|
|
131
|
+
expires_at = expires_at.call(request).to_f
|
|
132
|
+
raise Error, "`:auth_header_expires_at` must be positive" unless expires_at.positive?
|
|
133
|
+
|
|
134
|
+
expires_at
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
94
139
|
def dynamic_auth_token?(auth_header_value)
|
|
95
140
|
auth_header_value&.respond_to?(:call)
|
|
96
141
|
end
|
|
@@ -150,7 +195,10 @@ module HTTPX
|
|
|
150
195
|
# otherwise, it means that the first request already passed here, so this request should
|
|
151
196
|
# use whatever was generated for it.
|
|
152
197
|
@auth_header_value_mtx.synchronize do
|
|
153
|
-
|
|
198
|
+
if request.auth_token_value == @auth_header_value
|
|
199
|
+
@auth_header_value = generate_auth_token
|
|
200
|
+
set_auth_header_expires_at(request)
|
|
201
|
+
end
|
|
154
202
|
end
|
|
155
203
|
|
|
156
204
|
request.unauthorize!
|