httpx 1.6.2 → 1.7.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/0_11_0.md +3 -3
- data/doc/release_notes/1_6_3.md +47 -0
- data/doc/release_notes/1_7_0.md +149 -0
- data/lib/httpx/adapters/datadog.rb +1 -1
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/sentry.rb +1 -1
- data/lib/httpx/altsvc.rb +3 -1
- data/lib/httpx/connection/http1.rb +14 -15
- data/lib/httpx/connection/http2.rb +16 -15
- data/lib/httpx/connection.rb +118 -110
- data/lib/httpx/domain_name.rb +1 -1
- data/lib/httpx/extensions.rb +0 -14
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +1 -1
- data/lib/httpx/loggable.rb +14 -2
- data/lib/httpx/options.rb +60 -17
- data/lib/httpx/plugins/auth/digest.rb +44 -4
- data/lib/httpx/plugins/auth.rb +87 -4
- data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
- data/lib/httpx/plugins/callbacks.rb +15 -1
- data/lib/httpx/plugins/cookies/cookie.rb +1 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -5
- data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +2 -2
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +162 -56
- data/lib/httpx/plugins/proxy/http.rb +37 -9
- data/lib/httpx/plugins/rate_limiter.rb +2 -2
- data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
- data/lib/httpx/plugins/response_cache.rb +16 -9
- data/lib/httpx/plugins/retries.rb +55 -16
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +59 -8
- data/lib/httpx/plugins/stream_bidi.rb +87 -22
- data/lib/httpx/pool.rb +65 -21
- data/lib/httpx/request.rb +13 -14
- data/lib/httpx/resolver/https.rb +100 -34
- data/lib/httpx/resolver/multi.rb +12 -27
- data/lib/httpx/resolver/native.rb +68 -38
- data/lib/httpx/resolver/resolver.rb +46 -29
- data/lib/httpx/resolver/system.rb +63 -39
- data/lib/httpx/resolver.rb +97 -29
- data/lib/httpx/response/body.rb +2 -0
- data/lib/httpx/response.rb +22 -6
- data/lib/httpx/selector.rb +44 -20
- data/lib/httpx/session.rb +23 -33
- data/lib/httpx/transcoder/body.rb +1 -1
- data/lib/httpx/transcoder/deflate.rb +13 -8
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
- data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
- data/lib/httpx/transcoder/multipart.rb +16 -8
- data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
- data/lib/httpx/transcoder/utils/deflater.rb +1 -2
- data/lib/httpx/transcoder.rb +4 -6
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +3 -0
- data/sig/chainable.rbs +3 -3
- data/sig/connection.rbs +13 -6
- data/sig/loggable.rbs +5 -1
- data/sig/options.rbs +6 -2
- data/sig/plugins/auth/digest.rbs +6 -0
- data/sig/plugins/auth.rbs +28 -4
- data/sig/plugins/basic_auth.rbs +3 -3
- data/sig/plugins/callbacks.rbs +3 -0
- data/sig/plugins/digest_auth.rbs +2 -4
- data/sig/plugins/fiber_concurrency.rbs +6 -0
- data/sig/plugins/ntlm_auth.rbs +2 -2
- data/sig/plugins/oauth.rbs +46 -15
- data/sig/plugins/rate_limiter.rbs +1 -1
- data/sig/plugins/response_cache/file_store.rbs +2 -0
- data/sig/plugins/response_cache.rbs +4 -0
- data/sig/plugins/retries.rbs +8 -2
- data/sig/plugins/stream.rbs +13 -3
- data/sig/plugins/stream_bidi.rbs +5 -7
- data/sig/pool.rbs +1 -1
- data/sig/resolver/https.rbs +7 -0
- data/sig/resolver/multi.rbs +2 -9
- data/sig/resolver/native.rbs +1 -1
- data/sig/resolver/resolver.rbs +9 -8
- data/sig/resolver/system.rbs +4 -2
- data/sig/resolver.rbs +12 -3
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +2 -0
- data/sig/session.rbs +8 -8
- data/sig/transcoder/multipart.rbs +4 -2
- data/sig/transcoder.rbs +5 -1
- metadata +5 -1
|
@@ -47,6 +47,17 @@ module HTTPX
|
|
|
47
47
|
super || @state == :connecting || @state == :connected
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def force_close(*)
|
|
51
|
+
if @state == :connecting
|
|
52
|
+
# proxy connect related requests should not be reenqueed
|
|
53
|
+
@parser.reset!
|
|
54
|
+
@inflight -= @parser.pending.size
|
|
55
|
+
@parser.pending.clear
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
private
|
|
51
62
|
|
|
52
63
|
def handle_transition(nextstate)
|
|
@@ -64,23 +75,40 @@ module HTTPX
|
|
|
64
75
|
parser = @parser
|
|
65
76
|
parser.extend(ProxyParser)
|
|
66
77
|
parser.on(:response, &method(:__http_on_connect))
|
|
67
|
-
parser.on(:close) do
|
|
78
|
+
parser.on(:close) do
|
|
68
79
|
next unless @parser
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
emit(:terminate)
|
|
73
|
-
end
|
|
81
|
+
reset
|
|
82
|
+
disconnect
|
|
74
83
|
end
|
|
75
84
|
parser.on(:reset) do
|
|
76
85
|
if parser.empty?
|
|
77
86
|
reset
|
|
78
87
|
else
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
enqueue_pending_requests_from_parser(parser)
|
|
89
|
+
|
|
90
|
+
initial_state = @state
|
|
91
|
+
|
|
92
|
+
reset
|
|
93
|
+
|
|
94
|
+
if @pending.empty?
|
|
95
|
+
@parser = nil
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
# keep parser state around due to proxy auth protocol;
|
|
99
|
+
# intermediate authenticated request is already inside
|
|
100
|
+
# the parser
|
|
101
|
+
parser = nil
|
|
102
|
+
|
|
103
|
+
if initial_state == :connecting
|
|
104
|
+
parser = @parser
|
|
105
|
+
@parser.reset
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
idling
|
|
109
|
+
|
|
110
|
+
@parser = parser
|
|
81
111
|
|
|
82
|
-
parser.reset if @parser
|
|
83
|
-
transition(:idle)
|
|
84
112
|
transition(:connecting)
|
|
85
113
|
end
|
|
86
114
|
end
|
|
@@ -18,11 +18,11 @@ module HTTPX
|
|
|
18
18
|
def configure(klass)
|
|
19
19
|
klass.plugin(:retries,
|
|
20
20
|
retry_change_requests: true,
|
|
21
|
-
retry_on: method(:retry_on_rate_limited_response),
|
|
21
|
+
retry_on: method(:retry_on_rate_limited_response?),
|
|
22
22
|
retry_after: method(:retry_after_rate_limit))
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def retry_on_rate_limited_response(response)
|
|
25
|
+
def retry_on_rate_limited_response?(response)
|
|
26
26
|
return false unless response.is_a?(Response)
|
|
27
27
|
|
|
28
28
|
status = response.status
|
|
@@ -130,6 +130,7 @@ module HTTPX::Plugins
|
|
|
130
130
|
response = request.options.response_class.new(request, status, version, response_headers)
|
|
131
131
|
response.original_request = original_request
|
|
132
132
|
response.finish!
|
|
133
|
+
response.mark_as_cached!
|
|
133
134
|
|
|
134
135
|
IO.copy_stream(f, response.body)
|
|
135
136
|
|
|
@@ -118,7 +118,10 @@ module HTTPX
|
|
|
118
118
|
|
|
119
119
|
response.copy_from_cached!
|
|
120
120
|
elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
|
|
121
|
-
|
|
121
|
+
unless response.cached?
|
|
122
|
+
log { "caching response for #{request.uri}..." }
|
|
123
|
+
request.options.response_cache_store.set(request, response)
|
|
124
|
+
end
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
response
|
|
@@ -147,7 +150,7 @@ module HTTPX
|
|
|
147
150
|
request.headers.add("if-modified-since", last_modified)
|
|
148
151
|
end
|
|
149
152
|
|
|
150
|
-
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"])
|
|
153
|
+
if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"])
|
|
151
154
|
request.headers.add("if-none-match", etag)
|
|
152
155
|
end
|
|
153
156
|
end
|
|
@@ -204,7 +207,7 @@ module HTTPX
|
|
|
204
207
|
# returns a unique cache key as a String identifying this request
|
|
205
208
|
def response_cache_key
|
|
206
209
|
@response_cache_key ||= begin
|
|
207
|
-
keys = [@verb, @uri]
|
|
210
|
+
keys = [@verb, @uri.merge(path)]
|
|
208
211
|
|
|
209
212
|
@options.supported_vary_headers.each do |field|
|
|
210
213
|
value = @headers[field]
|
|
@@ -293,9 +296,7 @@ module HTTPX
|
|
|
293
296
|
return @cache_control if defined?(@cache_control)
|
|
294
297
|
|
|
295
298
|
@cache_control = begin
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
@headers["cache-control"].split(/ *, */)
|
|
299
|
+
@headers["cache-control"].split(/ *, */) if @headers.key?("cache-control")
|
|
299
300
|
end
|
|
300
301
|
end
|
|
301
302
|
|
|
@@ -304,9 +305,7 @@ module HTTPX
|
|
|
304
305
|
return @vary if defined?(@vary)
|
|
305
306
|
|
|
306
307
|
@vary = begin
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
@headers["vary"].split(/ *, */).map(&:downcase)
|
|
308
|
+
@headers["vary"].split(/ *, */).map(&:downcase) if @headers.key?("vary")
|
|
310
309
|
end
|
|
311
310
|
end
|
|
312
311
|
|
|
@@ -327,6 +326,14 @@ module HTTPX
|
|
|
327
326
|
Time.now
|
|
328
327
|
end
|
|
329
328
|
end
|
|
329
|
+
|
|
330
|
+
module ResponseBodyMethods
|
|
331
|
+
def decode_chunk(chunk)
|
|
332
|
+
return chunk if @response.cached?
|
|
333
|
+
|
|
334
|
+
super
|
|
335
|
+
end
|
|
336
|
+
end
|
|
330
337
|
end
|
|
331
338
|
register_plugin :response_cache, ResponseCache
|
|
332
339
|
end
|
|
@@ -36,15 +36,33 @@ module HTTPX
|
|
|
36
36
|
Parser::Error,
|
|
37
37
|
TimeoutError,
|
|
38
38
|
]).freeze
|
|
39
|
-
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }.freeze
|
|
41
|
+
|
|
42
|
+
# list of supported backoff algorithms
|
|
43
|
+
BACKOFF_ALGORITHMS = %i[exponential_backoff polynomial_backoff].freeze
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
if ENV.key?("HTTPX_NO_JITTER")
|
|
47
|
+
def extra_options(options)
|
|
48
|
+
options.merge(max_retries: MAX_RETRIES)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
def extra_options(options)
|
|
52
|
+
options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# returns the time to wait before resending +request+ as per the polynomial backoff retry strategy.
|
|
57
|
+
def retry_after_polynomial_backoff(request, _)
|
|
58
|
+
offset = request.options.max_retries - request.retries
|
|
59
|
+
2 * (offset - 1)
|
|
44
60
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
|
|
62
|
+
# returns the time to wait before resending +request+ as per the exponential backoff retry strategy.
|
|
63
|
+
def retry_after_exponential_backoff(request, _)
|
|
64
|
+
offset = request.options.max_retries - request.retries
|
|
65
|
+
(offset - 1) * 2
|
|
48
66
|
end
|
|
49
67
|
end
|
|
50
68
|
|
|
@@ -53,6 +71,7 @@ module HTTPX
|
|
|
53
71
|
# :max_retries :: max number of times a request will be retried (defaults to <tt>3</tt>).
|
|
54
72
|
# :retry_change_requests :: whether idempotent requests are retried (defaults to <tt>false</tt>).
|
|
55
73
|
# :retry_after:: seconds after which a request is retried; can also be a callable object (i.e. <tt>->(req, res) { ... } </tt>)
|
|
74
|
+
# or the name of a supported backoff algorithm (i.e. <tt>:exponential_backoff</tt>).
|
|
56
75
|
# :retry_jitter :: number of seconds applied to *:retry_after* (must be a callable, i.e. <tt>->(retry_after) { ... } </tt>).
|
|
57
76
|
# :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
|
|
58
77
|
# (i.e. <tt>->(res) { ... }</tt>).
|
|
@@ -60,10 +79,26 @@ module HTTPX
|
|
|
60
79
|
private
|
|
61
80
|
|
|
62
81
|
def option_retry_after(value)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
if value.respond_to?(:call)
|
|
83
|
+
value1 = value
|
|
84
|
+
value1 = value1.method(:call) unless value1.respond_to?(:arity)
|
|
85
|
+
|
|
86
|
+
# allow ->(*) arity as well, which is < 0
|
|
87
|
+
raise TypeError, "`:retry_after` proc has invalid number of parameters" unless value1.arity.negative? || value1.arity.between?(
|
|
88
|
+
1, 2
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
else
|
|
92
|
+
case value
|
|
93
|
+
when Symbol
|
|
94
|
+
raise TypeError, "`retry_after`: `#{value}` is not a supported backoff algorithm" unless BACKOFF_ALGORITHMS.include?(value)
|
|
95
|
+
|
|
96
|
+
value = Retries.method(:"retry_after_#{value}")
|
|
97
|
+
|
|
98
|
+
else
|
|
99
|
+
value = Float(value)
|
|
100
|
+
raise TypeError, "`:retry_after` must be positive" unless value.positive?
|
|
101
|
+
end
|
|
67
102
|
end
|
|
68
103
|
|
|
69
104
|
value
|
|
@@ -112,14 +147,13 @@ module HTTPX
|
|
|
112
147
|
(
|
|
113
148
|
response.is_a?(ErrorResponse) && retryable_error?(response.error)
|
|
114
149
|
) ||
|
|
115
|
-
|
|
116
|
-
options.retry_on
|
|
117
|
-
|
|
150
|
+
|
|
151
|
+
options.retry_on&.call(response)
|
|
152
|
+
|
|
118
153
|
)
|
|
119
154
|
try_partial_retry(request, response)
|
|
120
155
|
log { "failed to get response, #{request.retries} tries to go..." }
|
|
121
|
-
request
|
|
122
|
-
request.transition(:idle)
|
|
156
|
+
prepare_to_retry(request, response)
|
|
123
157
|
|
|
124
158
|
retry_after = options.retry_after
|
|
125
159
|
retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
|
|
@@ -165,6 +199,11 @@ module HTTPX
|
|
|
165
199
|
super && !request.retries.positive?
|
|
166
200
|
end
|
|
167
201
|
|
|
202
|
+
def prepare_to_retry(request, _response)
|
|
203
|
+
request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
|
|
204
|
+
request.transition(:idle)
|
|
205
|
+
end
|
|
206
|
+
|
|
168
207
|
#
|
|
169
208
|
# Attempt to set the request to perform a partial range request.
|
|
170
209
|
# This happens if the peer server accepts byte-range requests, and
|
data/lib/httpx/plugins/stream.rb
CHANGED
|
@@ -55,9 +55,9 @@ module HTTPX
|
|
|
55
55
|
line << chunk
|
|
56
56
|
|
|
57
57
|
while (idx = line.index("\n"))
|
|
58
|
-
yield line.byteslice(0..idx - 1)
|
|
58
|
+
yield line.byteslice(0..(idx - 1))
|
|
59
59
|
|
|
60
|
-
line = line.byteslice(idx + 1..-1)
|
|
60
|
+
line = line.byteslice((idx + 1)..-1)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -121,20 +121,71 @@ module HTTPX
|
|
|
121
121
|
# https://gitlab.com/os85/httpx/wikis/Stream
|
|
122
122
|
#
|
|
123
123
|
module Stream
|
|
124
|
+
STREAM_REQUEST_OPTIONS = { timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 }.freeze }.freeze
|
|
125
|
+
|
|
124
126
|
def self.extra_options(options)
|
|
125
|
-
options.merge(
|
|
127
|
+
options.merge(
|
|
128
|
+
stream: false,
|
|
129
|
+
timeout: { read_timeout: Float::INFINITY, operation_timeout: 60 },
|
|
130
|
+
stream_response_class: Class.new(StreamResponse, &Options::SET_TEMPORARY_NAME).freeze
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# adds support for the following options:
|
|
135
|
+
#
|
|
136
|
+
# :stream :: whether the request to process should be handled as a stream (defaults to <tt>false</tt>).
|
|
137
|
+
# :stream_response_class :: Class used to build the stream response object.
|
|
138
|
+
module OptionsMethods
|
|
139
|
+
def option_stream(val)
|
|
140
|
+
val
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def option_stream_response_class(value)
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def extend_with_plugin_classes(pl)
|
|
148
|
+
return super unless defined?(pl::StreamResponseMethods)
|
|
149
|
+
|
|
150
|
+
@stream_response_class = @stream_response_class.dup
|
|
151
|
+
Options::SET_TEMPORARY_NAME[@stream_response_class, pl]
|
|
152
|
+
@stream_response_class.__send__(:include, pl::StreamResponseMethods) if defined?(pl::StreamResponseMethods)
|
|
153
|
+
|
|
154
|
+
super
|
|
155
|
+
end
|
|
126
156
|
end
|
|
127
157
|
|
|
128
158
|
module InstanceMethods
|
|
129
|
-
def request(*args,
|
|
130
|
-
|
|
159
|
+
def request(*args, **options)
|
|
160
|
+
if args.first.is_a?(Request)
|
|
161
|
+
requests = args
|
|
162
|
+
|
|
163
|
+
request = requests.first
|
|
164
|
+
|
|
165
|
+
unless request.options.stream && !request.stream
|
|
166
|
+
if options[:stream]
|
|
167
|
+
warn "passing `stream: true` with a request obkect is not supported anymore. " \
|
|
168
|
+
"You can instead build the request object with `stream :true`"
|
|
169
|
+
end
|
|
170
|
+
return super
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
return super unless options[:stream]
|
|
174
|
+
|
|
175
|
+
requests = build_requests(*args, options)
|
|
176
|
+
|
|
177
|
+
request = requests.first
|
|
178
|
+
end
|
|
131
179
|
|
|
132
|
-
requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
|
|
133
180
|
raise Error, "only 1 response at a time is supported for streaming requests" unless requests.size == 1
|
|
134
181
|
|
|
135
|
-
request
|
|
182
|
+
@options.stream_response_class.new(request, self)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_request(verb, uri, params = EMPTY_HASH, options = @options)
|
|
186
|
+
return super unless params[:stream]
|
|
136
187
|
|
|
137
|
-
|
|
188
|
+
super(verb, uri, params, options.merge(STREAM_REQUEST_OPTIONS.merge(stream: true)))
|
|
138
189
|
end
|
|
139
190
|
end
|
|
140
191
|
|
|
@@ -16,7 +16,7 @@ module HTTPX
|
|
|
16
16
|
# The streams keeps send DATA frames while there's data; when they're ain't,
|
|
17
17
|
# the stream is kept open; it must be explicitly closed by the end user.
|
|
18
18
|
#
|
|
19
|
-
|
|
19
|
+
module HTTP2Methods
|
|
20
20
|
def initialize(*)
|
|
21
21
|
super
|
|
22
22
|
@lock = Thread::Mutex.new
|
|
@@ -26,6 +26,8 @@ module HTTPX
|
|
|
26
26
|
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
27
27
|
# lock.aware version of +#{lock_meth}+
|
|
28
28
|
def #{lock_meth}(*) # def close(*)
|
|
29
|
+
return super unless @options.stream
|
|
30
|
+
|
|
29
31
|
return super if @lock.owned?
|
|
30
32
|
|
|
31
33
|
# small race condition between
|
|
@@ -43,6 +45,8 @@ module HTTPX
|
|
|
43
45
|
class_eval(<<-METH, __FILE__, __LINE__ + 1)
|
|
44
46
|
# lock.aware version of +#{lock_meth}+
|
|
45
47
|
private def #{lock_meth}(*) # private def join_headers(*)
|
|
48
|
+
return super unless @options.stream
|
|
49
|
+
|
|
46
50
|
return super if @lock.owned?
|
|
47
51
|
|
|
48
52
|
# small race condition between
|
|
@@ -55,6 +59,8 @@ module HTTPX
|
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def handle_stream(stream, request)
|
|
62
|
+
return super unless @options.stream
|
|
63
|
+
|
|
58
64
|
request.on(:body) do
|
|
59
65
|
next unless request.headers_sent
|
|
60
66
|
|
|
@@ -67,6 +73,8 @@ module HTTPX
|
|
|
67
73
|
|
|
68
74
|
# when there ain't more chunks, it makes the buffer as full.
|
|
69
75
|
def send_chunk(request, stream, chunk, next_chunk)
|
|
76
|
+
return super unless @options.stream
|
|
77
|
+
|
|
70
78
|
super
|
|
71
79
|
|
|
72
80
|
return if next_chunk
|
|
@@ -77,39 +85,60 @@ module HTTPX
|
|
|
77
85
|
|
|
78
86
|
# sets end-stream flag when the request is closed.
|
|
79
87
|
def end_stream?(request, next_chunk)
|
|
88
|
+
return super unless @options.stream
|
|
89
|
+
|
|
80
90
|
request.closed? && next_chunk.nil?
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
|
|
84
|
-
# BidiBuffer is a Buffer which can
|
|
85
|
-
# than the thread of the corresponding Connection/Session.
|
|
94
|
+
# BidiBuffer is a thread-safe Buffer which can receive data from any thread.
|
|
86
95
|
#
|
|
87
|
-
# It
|
|
88
|
-
#
|
|
96
|
+
# It uses a dual-buffer strategy with mutex protection:
|
|
97
|
+
# - +@buffer+ is the main buffer, protected by +@buffer_mutex+
|
|
98
|
+
# - +@oob_buffer+ receives data when +@buffer_mutex+ is contended
|
|
99
|
+
#
|
|
100
|
+
# This allows non-blocking writes from any thread while maintaining thread safety.
|
|
89
101
|
class BidiBuffer < Buffer
|
|
90
102
|
def initialize(*)
|
|
91
103
|
super
|
|
92
|
-
@
|
|
104
|
+
@buffer_mutex = Thread::Mutex.new
|
|
93
105
|
@oob_mutex = Thread::Mutex.new
|
|
94
106
|
@oob_buffer = "".b
|
|
95
107
|
end
|
|
96
108
|
|
|
97
|
-
# buffers the +chunk+ to be sent
|
|
109
|
+
# buffers the +chunk+ to be sent (thread-safe, non-blocking)
|
|
98
110
|
def <<(chunk)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
if @buffer_mutex.try_lock
|
|
112
|
+
begin
|
|
113
|
+
super
|
|
114
|
+
ensure
|
|
115
|
+
@buffer_mutex.unlock
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
# another thread holds the lock, use OOB buffer to avoid blocking
|
|
119
|
+
@oob_mutex.synchronize { @oob_buffer << chunk }
|
|
120
|
+
end
|
|
102
121
|
end
|
|
103
122
|
|
|
104
|
-
# reconciles the main and secondary buffer (
|
|
123
|
+
# reconciles the main and secondary buffer (thread-safe, callable from any thread).
|
|
105
124
|
def rebuffer
|
|
106
|
-
|
|
125
|
+
@buffer_mutex.synchronize do
|
|
126
|
+
@oob_mutex.synchronize do
|
|
127
|
+
return if @oob_buffer.empty?
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
@buffer << @oob_buffer
|
|
130
|
+
@oob_buffer.clear
|
|
131
|
+
end
|
|
111
132
|
end
|
|
112
133
|
end
|
|
134
|
+
|
|
135
|
+
Buffer.instance_methods - Object.instance_methods - %i[<<].each do |meth|
|
|
136
|
+
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
|
137
|
+
def #{meth}(*) # def empty?
|
|
138
|
+
@buffer_mutex.synchronize { super }
|
|
139
|
+
end
|
|
140
|
+
MOD
|
|
141
|
+
end
|
|
113
142
|
end
|
|
114
143
|
|
|
115
144
|
# Proxy to wake up the session main loop when one
|
|
@@ -117,8 +146,11 @@ module HTTPX
|
|
|
117
146
|
# which allows it to be registered in the selector alongside actual HTTP-based
|
|
118
147
|
# HTTPX::Connection objects.
|
|
119
148
|
class Signal
|
|
149
|
+
attr_reader :error
|
|
150
|
+
|
|
120
151
|
def initialize
|
|
121
152
|
@closed = false
|
|
153
|
+
@error = nil
|
|
122
154
|
@pipe_read, @pipe_write = IO.pipe
|
|
123
155
|
end
|
|
124
156
|
|
|
@@ -153,12 +185,23 @@ module HTTPX
|
|
|
153
185
|
|
|
154
186
|
def timeout; end
|
|
155
187
|
|
|
188
|
+
def inflight?
|
|
189
|
+
!@closed
|
|
190
|
+
end
|
|
191
|
+
|
|
156
192
|
def terminate
|
|
193
|
+
return if @closed
|
|
194
|
+
|
|
157
195
|
@pipe_write.close
|
|
158
196
|
@pipe_read.close
|
|
159
197
|
@closed = true
|
|
160
198
|
end
|
|
161
199
|
|
|
200
|
+
def on_error(error)
|
|
201
|
+
@error = error
|
|
202
|
+
terminate
|
|
203
|
+
end
|
|
204
|
+
|
|
162
205
|
# noop (the owner connection will take of it)
|
|
163
206
|
def handle_socket_timeout(interval); end
|
|
164
207
|
end
|
|
@@ -182,17 +225,22 @@ module HTTPX
|
|
|
182
225
|
def close(selector = Selector.new)
|
|
183
226
|
@signal.terminate
|
|
184
227
|
selector.deregister(@signal)
|
|
185
|
-
super
|
|
228
|
+
super
|
|
186
229
|
end
|
|
187
230
|
|
|
188
231
|
def select_connection(connection, selector)
|
|
232
|
+
return super unless connection.options.stream
|
|
233
|
+
|
|
189
234
|
super
|
|
190
235
|
selector.register(@signal)
|
|
191
236
|
connection.signal = @signal
|
|
192
237
|
end
|
|
193
238
|
|
|
194
239
|
def deselect_connection(connection, *)
|
|
240
|
+
return super unless connection.options.stream
|
|
241
|
+
|
|
195
242
|
super
|
|
243
|
+
|
|
196
244
|
connection.signal = nil
|
|
197
245
|
end
|
|
198
246
|
end
|
|
@@ -210,10 +258,14 @@ module HTTPX
|
|
|
210
258
|
end
|
|
211
259
|
|
|
212
260
|
def closed?
|
|
261
|
+
return super unless @options.stream
|
|
262
|
+
|
|
213
263
|
@closed
|
|
214
264
|
end
|
|
215
265
|
|
|
216
266
|
def can_buffer?
|
|
267
|
+
return super unless @options.stream
|
|
268
|
+
|
|
217
269
|
super && @state != :waiting_for_chunk
|
|
218
270
|
end
|
|
219
271
|
|
|
@@ -221,6 +273,8 @@ module HTTPX
|
|
|
221
273
|
# +:waiting_for_chunk+ state, which the request transitions to once payload
|
|
222
274
|
# is buffered.
|
|
223
275
|
def transition(nextstate)
|
|
276
|
+
return super unless @options.stream
|
|
277
|
+
|
|
224
278
|
headers_sent = @headers_sent
|
|
225
279
|
|
|
226
280
|
case nextstate
|
|
@@ -255,6 +309,8 @@ module HTTPX
|
|
|
255
309
|
end
|
|
256
310
|
|
|
257
311
|
def close
|
|
312
|
+
return super unless @options.stream
|
|
313
|
+
|
|
258
314
|
@mutex.synchronize do
|
|
259
315
|
return if @closed
|
|
260
316
|
|
|
@@ -269,10 +325,12 @@ module HTTPX
|
|
|
269
325
|
module RequestBodyMethods
|
|
270
326
|
def initialize(*, **)
|
|
271
327
|
super
|
|
272
|
-
@headers.delete("content-length")
|
|
328
|
+
@headers.delete("content-length") if @options.stream
|
|
273
329
|
end
|
|
274
330
|
|
|
275
331
|
def empty?
|
|
332
|
+
return super unless @options.stream
|
|
333
|
+
|
|
276
334
|
false
|
|
277
335
|
end
|
|
278
336
|
end
|
|
@@ -284,25 +342,32 @@ module HTTPX
|
|
|
284
342
|
|
|
285
343
|
def initialize(*)
|
|
286
344
|
super
|
|
345
|
+
|
|
346
|
+
return unless @options.stream
|
|
347
|
+
|
|
287
348
|
@write_buffer = BidiBuffer.new(@options.buffer_size)
|
|
288
349
|
end
|
|
289
350
|
|
|
290
351
|
# rebuffers the +@write_buffer+ before calculating interests.
|
|
291
352
|
def interests
|
|
353
|
+
return super unless @options.stream
|
|
354
|
+
|
|
292
355
|
@write_buffer.rebuffer
|
|
293
356
|
|
|
294
357
|
super
|
|
295
358
|
end
|
|
296
359
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def parser_type(protocol)
|
|
300
|
-
return HTTP2Bidi if protocol == "h2"
|
|
360
|
+
def call
|
|
361
|
+
return super unless @options.stream && (error = @signal.error)
|
|
301
362
|
|
|
302
|
-
|
|
363
|
+
on_error(error)
|
|
303
364
|
end
|
|
304
365
|
|
|
366
|
+
private
|
|
367
|
+
|
|
305
368
|
def set_parser_callbacks(parser)
|
|
369
|
+
return super unless @options.stream
|
|
370
|
+
|
|
306
371
|
super
|
|
307
372
|
parser.on(:flush_buffer) do
|
|
308
373
|
@signal.wakeup if @signal
|