httpx 1.8.0 → 1.8.2

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +1 -2
  3. data/doc/release_notes/1_8_1.md +27 -0
  4. data/doc/release_notes/1_8_2.md +30 -0
  5. data/lib/httpx/adapters/datadog.rb +9 -0
  6. data/lib/httpx/adapters/faraday.rb +2 -2
  7. data/lib/httpx/adapters/webmock.rb +8 -1
  8. data/lib/httpx/buffer.rb +5 -0
  9. data/lib/httpx/connection/http1.rb +6 -12
  10. data/lib/httpx/connection/http2.rb +38 -10
  11. data/lib/httpx/connection.rb +86 -27
  12. data/lib/httpx/errors.rb +3 -0
  13. data/lib/httpx/headers.rb +2 -2
  14. data/lib/httpx/io/ssl.rb +26 -17
  15. data/lib/httpx/io/tcp.rb +11 -4
  16. data/lib/httpx/io/unix.rb +2 -2
  17. data/lib/httpx/loggable.rb +11 -4
  18. data/lib/httpx/options.rb +6 -2
  19. data/lib/httpx/plugins/auth/digest.rb +1 -1
  20. data/lib/httpx/plugins/auth.rb +17 -7
  21. data/lib/httpx/plugins/callbacks.rb +12 -0
  22. data/lib/httpx/plugins/digest_auth.rb +4 -0
  23. data/lib/httpx/plugins/fiber_concurrency.rb +25 -1
  24. data/lib/httpx/plugins/follow_redirects.rb +21 -9
  25. data/lib/httpx/plugins/oauth.rb +4 -2
  26. data/lib/httpx/plugins/persistent.rb +33 -2
  27. data/lib/httpx/plugins/proxy.rb +9 -3
  28. data/lib/httpx/plugins/push_promise.rb +2 -2
  29. data/lib/httpx/plugins/retries.rb +15 -8
  30. data/lib/httpx/plugins/server_sent_events.rb +1 -1
  31. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  32. data/lib/httpx/plugins/stream.rb +2 -2
  33. data/lib/httpx/plugins/stream_bidi.rb +2 -0
  34. data/lib/httpx/plugins/tracing.rb +17 -4
  35. data/lib/httpx/pool.rb +2 -2
  36. data/lib/httpx/request/body.rb +10 -7
  37. data/lib/httpx/request.rb +27 -12
  38. data/lib/httpx/resolver/cache/base.rb +1 -8
  39. data/lib/httpx/resolver/https.rb +20 -20
  40. data/lib/httpx/resolver/native.rb +11 -0
  41. data/lib/httpx/resolver/resolver.rb +4 -0
  42. data/lib/httpx/resolver/system.rb +5 -2
  43. data/lib/httpx/response/body.rb +2 -2
  44. data/lib/httpx/response.rb +6 -7
  45. data/lib/httpx/selector.rb +6 -1
  46. data/lib/httpx/session.rb +11 -3
  47. data/lib/httpx/session_extensions.rb +2 -2
  48. data/lib/httpx/timers.rb +3 -0
  49. data/lib/httpx/transcoder/json.rb +1 -2
  50. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  51. data/lib/httpx/version.rb +1 -1
  52. data/sig/connection/http2.rbs +3 -0
  53. data/sig/connection.rbs +8 -3
  54. data/sig/errors.rbs +2 -2
  55. data/sig/io/ssl.rbs +5 -0
  56. data/sig/loggable.rbs +4 -0
  57. data/sig/plugins/auth.rbs +7 -3
  58. data/sig/plugins/persistent.rbs +7 -0
  59. data/sig/plugins/retries.rbs +4 -2
  60. data/sig/plugins/tracing.rbs +1 -1
  61. data/sig/request.rbs +2 -3
  62. data/sig/resolver/system.rbs +3 -0
  63. data/sig/selector.rbs +2 -0
  64. data/sig/timers.rbs +2 -0
  65. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74104ce6c4d05053e2a005974de5f0cd66346a1cb4558b7c0887d3a2d691d226
4
- data.tar.gz: ca0ce3e30f509d44e4c0daac30a0b5d9949c118c19a715561c276f1debaf5dbf
3
+ metadata.gz: c34100ea496b94c4dbcfcd95330a5c8fa21dd5997d49dff54f62ced363589f7f
4
+ data.tar.gz: 24ef436a40b17cf4df3b2a9722c36437704c0c2ed1f5a1afc59b16801c36dbb4
5
5
  SHA512:
6
- metadata.gz: 0de3c6baf56c86272cc45bdc06e1187655e759ddf329414d71fcbf4603c9d0e03791641e05f5c07ef84cd578a3bba1a855fde77b5b0f1f2e11df1cf60ea0815a
7
- data.tar.gz: a276c6ed937c8d04103382d517d56d9df805886f23b05a38f6e654fcd6935e17407101e1eedc2b6605b4f347a37262116024a61633ebdd660131b06fc1f3fde9
6
+ metadata.gz: 7796301721da3f362b4ec13bf5bffcdec864819dddf404dfb7c7ffadde69508de32e52b85e163b144d4ade6b3579aa256107680332a264b27cbe67fdd9e23875
7
+ data.tar.gz: 63f47f3e7c9e50b720c8637dabc506fe30e79fb403e6d658646ff6fb894dc02774c8e1c2491d938392f1184d8f26f681361f9ffd1dec6675a622c03ba0441342
@@ -17,5 +17,4 @@
17
17
  * `:stream_bidi` plugin: fix internal state preventing bidi requests from being retried.
18
18
  * `:stream_bidi` plugin: will only allow initial request body being passed using `:body` param (others, like `:json`, will raise an exception)
19
19
  * https resolver: return `:idle` on `#state` calls when no connection is available (sometimes called in internal log messages).
20
- * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
21
- *
20
+ * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
@@ -0,0 +1,27 @@
1
+ # 1.8.1
2
+
3
+ ## Improvements
4
+
5
+ * the session will now eagerly write initial data to connections instead of waiting for I/O the first time (saves a syscall).
6
+ * removed all rescue blocks rescuing `StandardError` internally; this could interfere with usages of `httpx` inside `Timeout.timeout` blocks, which may emit an exception at any point, including in blocks where that exception could be rescued, leaving the session in a potentially irrecoverable state and looping.
7
+ * debug logs change the display of ids from 'tid:123, fid:123' to 'tid=123 fid:123'.
8
+ * and when using the `datadog` adapter, all related correlation ids (trace id, span id, etc) will also be added to the identifiers in the same format
9
+ * plugins built on top of the `:retries` plugin will skip the `retry_after` interval when certain conditions are met.
10
+ * `:persistent` plugin: request will be retried immediately, when the error happens after the keep alive interval expired.
11
+ * `:auth` plugin: request will be retried immediately if it failed due to a "401 Unauthorized Error".
12
+ * `:ssrf_filter` plugin: removed public/private references of exception message.
13
+
14
+ ## Bugfixes
15
+
16
+ * `:auth` plugin: handle edge case where `:auth_header_expires_at` returns no value.
17
+ * `:digest_auth` plugin: do not retry if there was an issue building building the authorization header after the probe.
18
+ * `webmock` adapter: prevent mocked requests from opening a connection to the peer server that will not be used.
19
+ * clear and cancel request timers on completion or reset, ensuring that they won't be retained way past the point where the request is gone.
20
+ * connection: cancel and discard ping timer before closing (removes timer reference)
21
+ * connection: fix logic where purging-while-resending requests to a coalesced connection was leaving references in the old connection.
22
+ * moved an `on_body_chunk` callback to the `:callbacks` plugin, where it's used and expected to be defined.
23
+ * `http2`: avoid closing the connection on internal errors which are supposed to be specific to a request, such as the max headers or max response body size checks.
24
+ * system resolver will use an internal exception when using `Timeout.timeout` on resolutions to avoid conflicts with the default timeout exception class.
25
+ * prevent edge case where `#alpn_protocol` was being called on a `TCPSocket`.
26
+ * fixed v1.8.0 regression causing connection to loop when an error happens while connecting.
27
+ * resolver cache: fixed cache eviction logic, where alias answers weren't being evicted, causing resolutions to stall after a certain number of resolutions.
@@ -0,0 +1,30 @@
1
+ # 1.8.2
2
+
3
+ ## Improvements
4
+
5
+ * `:proxy` plugin: allow setting `:no_proxy` option to `"*"` (or `["*"]`) to allow bypassing proxy for all requests.
6
+ * Extending sessions with unknown options will now raise an error, instead of silently ignoring them (consistent with the initialization case now):
7
+
8
+ ```ruby
9
+ session = HTTPX.with(unknown_option: 1) # will raise a HTTPX::Error: unknown option: unknown_option
10
+ ```
11
+
12
+ * Use `HTTP2::Connection#closed?` internally (instead of `#state`) to check for connection state; this will, in a future release of `http-2`, seamlessly support the new `:closing` state.
13
+ * Add missing `response` attribute reader to `HTTPX::RequestTimeoutError`.
14
+ * Improve correctness of calling `#dup` on requests and connections.
15
+
16
+ ## Bugfixes
17
+
18
+ * `:auth` plugin: generate token first, then set the "expires_at" timestamp (bearer tokens, specially if JWT, may carry information about when it expires).
19
+ * `:auth` plugin: do not check for token freshness on a retry, instead force token regeneration.
20
+ * `:retries` plugin: do not apply "retry after" delay after a ping timeout error (the ping timeout already implies a delay, so retry on a new connection should be immediate).
21
+ * `:retries` plugin: do not retry on any HTTP/2 frame error (only on GOAWAY or PING frame errors).
22
+ * `:persistent` plugin: do not decrement request remaining retries on ping or GOAWAY frame errors.
23
+ * `:tracing` plugin: set connection initialization time to nil no termination (so that it doesn't get reused on persistent connections after reuse).
24
+ * `:follow_redirects` plugin: fixed issue when a timeout error would be raised but only bubble up to the root request, not properly closing the in-flight follow up request, and leaving the request handling phase in a broken state.
25
+ * datadog adapter: the connection initialization time is now set only on connect or first request received, the reason, making it more accurate.
26
+ * webmock adapter: `.to_timeout` helper will make the request raise a `HTTPX::RequestTimeoutError` instead of a plain `TimeoutError` (otherwise, if using persistent sessions, it'd trigger a retry instead and mess with webmock request count bookkeeping).
27
+ * native resolver: do not transition to `:open` if the socket was closed during resolution (which happens when an error is raised while the query bytes are buffered).
28
+ * fix for when setting a new session callback on an ssl context object which has been frozen after the first connection attempt (done by binding the ssl context callback to an internal ivar which can be changed).
29
+ * connection: protect against `Errno::ETIMEDOUT` errors on socket close by forcing transition to closed.
30
+ * do not set a ping timer for HTTP/1.1 on keep alive flow, as there's no ping and the connection is reestablished.
@@ -185,6 +185,13 @@ module Datadog::Tracing
185
185
  end
186
186
  end
187
187
 
188
+ # patches httpx debug logs to include datadog correlation ids
189
+ module LoggablePatch
190
+ def log_identifiers
191
+ "#{super} #{Datadog::Tracing.log_correlation}"
192
+ end
193
+ end
194
+
188
195
  module Configuration
189
196
  # Default settings for httpx
190
197
  #
@@ -297,6 +304,8 @@ module Datadog::Tracing
297
304
 
298
305
  ::HTTPX.send(:remove_const, :Session)
299
306
  ::HTTPX.send(:const_set, :Session, datadog_session.class)
307
+
308
+ ::HTTPX::Loggable.singleton_class.prepend(LoggablePatch)
300
309
  end
301
310
  end
302
311
 
@@ -115,11 +115,11 @@ module Faraday
115
115
  ssl_options
116
116
  end
117
117
  else
118
- # :nocov:
118
+ # simplecov:disable
119
119
  def ssl_options_from_env(*)
120
120
  {}
121
121
  end
122
- # :nocov:
122
+ # simplecov:enable
123
123
  end
124
124
  end
125
125
 
@@ -34,7 +34,8 @@ module WebMock
34
34
  end
35
35
 
36
36
  def build_from_webmock_response(request, webmock_response)
37
- return build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
37
+ return build_error_response(request,
38
+ HTTPX::RequestTimeoutError.new(request, webmock_response, 1)) if webmock_response.should_timeout
38
39
 
39
40
  return build_error_response(request, webmock_response.exception) if webmock_response.exception
40
41
 
@@ -155,6 +156,12 @@ module WebMock
155
156
  raise WebMock::NetConnectNotAllowedError, request_signature
156
157
  end
157
158
  end
159
+
160
+ private
161
+
162
+ def connect
163
+ super unless @mocked
164
+ end
158
165
  end
159
166
  end
160
167
 
data/lib/httpx/buffer.rb CHANGED
@@ -46,6 +46,11 @@ module HTTPX
46
46
  def_delegator :@buffer, :<<
47
47
  end
48
48
 
49
+ def initialize_dup(other)
50
+ super
51
+ @buffer = other.instance_variable_get(:@buffer).dup
52
+ end
53
+
49
54
  def full?
50
55
  @buffer.bytesize >= @limit
51
56
  end
@@ -29,7 +29,7 @@ module HTTPX
29
29
  @version = [1, 1]
30
30
  @pending = []
31
31
  @requests = []
32
- @request = nil
32
+ @callbacks = @request = nil
33
33
  @handshake_completed = @pipelining = false
34
34
  end
35
35
 
@@ -162,18 +162,12 @@ module HTTPX
162
162
 
163
163
  return unless request
164
164
 
165
- begin
166
- request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
167
- request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
165
+ request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
166
+ request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
168
167
 
169
- response = request.response
168
+ response = request.response
170
169
 
171
- response << chunk
172
- rescue StandardError => e
173
- error_response = ErrorResponse.new(request, e)
174
- request.response = error_response
175
- dispatch(request)
176
- end
170
+ response << chunk
177
171
  end
178
172
 
179
173
  def on_complete
@@ -416,7 +410,7 @@ module HTTPX
416
410
  end
417
411
 
418
412
  def capitalized(field)
419
- UPCASED[field] || field.split("-").map(&:capitalize).join("-")
413
+ UPCASED.fetch(field) { field.split("-").map(&:capitalize).join("-") }
420
414
  end
421
415
  end
422
416
  end
@@ -31,12 +31,14 @@ module HTTPX
31
31
  attr_reader :streams, :pending
32
32
 
33
33
  def initialize(buffer, options)
34
+ @callbacks = nil
34
35
  @options = options
35
36
  @settings = @options.http2_settings
36
37
  @pending = []
37
38
  @streams = {}
38
39
  @drains = {}
39
40
  @pings = []
41
+ @streams_to_close_after_receive = []
40
42
  @buffer = buffer
41
43
  @handshake_completed = false
42
44
  @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
@@ -52,7 +54,7 @@ module HTTPX
52
54
  end
53
55
 
54
56
  def interests
55
- if @connection.state == :closed
57
+ if @connection.closed?
56
58
  return unless @handshake_completed
57
59
 
58
60
  return if @buffer.empty?
@@ -93,7 +95,7 @@ module HTTPX
93
95
  end
94
96
 
95
97
  def close
96
- unless @connection.state == :closed
98
+ unless @connection.closed?
97
99
  @connection.goaway
98
100
  emit(:timeout, @options.timeout[:close_handshake_timeout])
99
101
  end
@@ -101,7 +103,7 @@ module HTTPX
101
103
  end
102
104
 
103
105
  def empty?
104
- @connection.state == :closed || @streams.empty?
106
+ @connection.closed? || @streams.empty?
105
107
  end
106
108
 
107
109
  def exhausted?
@@ -110,6 +112,12 @@ module HTTPX
110
112
 
111
113
  def <<(data)
112
114
  @connection << data
115
+
116
+ while (stream, request, error = @streams_to_close_after_receive.shift)
117
+ # these streams were marked for cancellation due to errors found while processing the
118
+ # data received by the peer.
119
+ emit_stream_error(stream, request, error)
120
+ end
113
121
  end
114
122
 
115
123
  def send(request, head = false)
@@ -128,7 +136,7 @@ module HTTPX
128
136
  rescue ::HTTP2::Error::StreamLimitExceeded
129
137
  @pending.unshift(request)
130
138
  false
131
- rescue StandardError => e
139
+ rescue ::HTTP2::Error::Error, ArgumentError => e
132
140
  emit(:error, request, e)
133
141
  end
134
142
 
@@ -333,6 +341,8 @@ module HTTPX
333
341
  @streams[request] = stream
334
342
 
335
343
  handle(request, stream) if request.expects?
344
+ rescue HTTPX::Error => e
345
+ @streams_to_close_after_receive << [stream, request, e]
336
346
  end
337
347
 
338
348
  def on_stream_trailers(stream, request, response, h)
@@ -345,7 +355,18 @@ module HTTPX
345
355
  def on_stream_data(stream, request, data)
346
356
  request.log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
347
357
  request.log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
358
+
359
+ return unless request.response
360
+
348
361
  request.response << data
362
+ rescue HTTPX::Error => e
363
+ if stream.state == :closing
364
+ # there won't be any more chunks to process after this one.
365
+ emit_stream_error(stream, request, e)
366
+ else
367
+ # defer until the last chunk from the payload is processed.
368
+ @streams_to_close_after_receive << [stream, request, e]
369
+ end
349
370
  end
350
371
 
351
372
  def on_stream_refuse(stream, request, error)
@@ -381,11 +402,12 @@ module HTTPX
381
402
  emit(:response, request, response)
382
403
  end
383
404
  else
384
- response = request.response
385
- if response && response.is_a?(Response) && response.status == 421
386
- emit(:error, request, :http_1_1_required)
387
- else
388
- emit(:response, request, response)
405
+ if (response = request.response)
406
+ if response.is_a?(Response) && response.status == 421
407
+ emit(:error, request, :http_1_1_required)
408
+ else
409
+ emit(:response, request, response)
410
+ end
389
411
  end
390
412
  end
391
413
  send(@pending.shift) unless @pending.empty?
@@ -411,7 +433,7 @@ module HTTPX
411
433
  end
412
434
 
413
435
  def on_close(_last_frame, error, _payload)
414
- is_connection_closed = @connection.state == :closed
436
+ is_connection_closed = @connection.closed?
415
437
  if error
416
438
  @buffer.clear if is_connection_closed
417
439
  case error
@@ -509,6 +531,12 @@ module HTTPX
509
531
  emit(:pong)
510
532
  end
511
533
 
534
+ def emit_stream_error(stream, request, error)
535
+ teardown(request)
536
+ stream.close
537
+ emit(:error, request, error)
538
+ end
539
+
512
540
  def teardown(request = nil)
513
541
  if request
514
542
  @drains.delete(request)
@@ -36,19 +36,20 @@ module HTTPX
36
36
 
37
37
  def_delegator :@write_buffer, :empty?
38
38
 
39
- attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling
39
+ attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling,
40
+ :read_buffer, :write_buffer
40
41
 
41
42
  attr_writer :current_selector
42
43
 
43
44
  attr_accessor :current_session, :family
44
45
 
45
- protected :ssl_session, :sibling
46
+ protected :ssl_session, :sibling, :read_buffer, :write_buffer
46
47
 
47
48
  def initialize(uri, options)
48
49
  @current_session = @current_selector = @max_concurrent_requests =
49
50
  @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
- @ping_timer = @family = @io = @ssl_session =
51
- @timeout = @connected_at = @response_received_at = nil
51
+ @callbacks = @ping_timer = @family =
52
+ @io = @ssl_session = @timeout = @connected_at = @response_received_at = nil
52
53
 
53
54
  @exhausted = @cloned = @main_sibling = false
54
55
 
@@ -76,6 +77,29 @@ module HTTPX
76
77
  self.addresses = @options.addresses if @options.addresses
77
78
  end
78
79
 
80
+ # dupped initialization
81
+ def initialize_dup(orig)
82
+ super
83
+ @callbacks = @parser = @sibling = @coalesced_connection = @altsvc_connection = nil
84
+ @origins = orig.origins.dup
85
+ @read_buffer = orig.read_buffer.dup
86
+ @write_buffer = orig.write_buffer.dup
87
+ @inflight = 0
88
+ @pending = []
89
+ transition(:idle)
90
+
91
+ if @io
92
+ # initialize new IO object with the same set of addresses
93
+ addresses = @io.addresses
94
+ @io = nil
95
+ self.addresses = addresses
96
+ end
97
+
98
+ return unless @current_session && @current_selector
99
+
100
+ @current_session.pin(self, @current_selector)
101
+ end
102
+
79
103
  def peer
80
104
  @origin
81
105
  end
@@ -148,11 +172,11 @@ module HTTPX
148
172
 
149
173
  def merge(connection)
150
174
  @origins |= connection.instance_variable_get(:@origins)
151
- if @ssl_session.nil? && connection.ssl_session
152
- @ssl_session = connection.ssl_session
153
- @io.session_new_cb do |sess|
154
- @ssl_session = sess
155
- end if @io
175
+ if @ssl_session.nil? && (ssl_session = connection.ssl_session)
176
+ @ssl_session = ssl_session
177
+ # the socket only needs the merged session if it can still resume it,
178
+ # i.e. if TLS hasn't been negotiated yet.
179
+ @io.ssl_session = ssl_session if @io.is_a?(SSL) && !@io.connected?
156
180
  end
157
181
  connection.purge_pending do |req|
158
182
  req.transition(:idle)
@@ -161,15 +185,17 @@ module HTTPX
161
185
  end
162
186
 
163
187
  def purge_pending(&block)
164
- pendings = []
165
188
  if @parser
166
189
  pending = @parser.pending
167
190
  @inflight -= pending.size
168
- pendings << pending
191
+ pending.reject! do |req|
192
+ block.call(req)
193
+ true
194
+ end
169
195
  end
170
- pendings << @pending
171
- pendings.each do |pending|
172
- pending.reject!(&block)
196
+ @pending.reject! do |req|
197
+ block.call(req)
198
+ true
173
199
  end
174
200
  end
175
201
 
@@ -203,7 +229,7 @@ module HTTPX
203
229
  return @parser.interests if @parser
204
230
 
205
231
  nil
206
- rescue StandardError => e
232
+ rescue Error => e
207
233
  on_error(e)
208
234
  nil
209
235
  end
@@ -215,6 +241,8 @@ module HTTPX
215
241
  def call
216
242
  case @state
217
243
  when :idle
244
+ return if no_more_requests?
245
+
218
246
  connect
219
247
 
220
248
  # when opening the tcp or ssl socket fails
@@ -222,7 +250,7 @@ module HTTPX
222
250
 
223
251
  consume
224
252
  when :closed
225
- return if @pending.empty?
253
+ return if no_more_requests?
226
254
 
227
255
  # there are pending requests to send, restart the state machine.
228
256
  idling
@@ -247,10 +275,14 @@ module HTTPX
247
275
  consume
248
276
  end
249
277
  nil
250
- rescue IOError => e
278
+ rescue Errno::ECONNRESET,
279
+ Errno::EINVAL,
280
+ SocketError,
281
+ IOError,
282
+ TLSError => e
251
283
  @write_buffer.clear
252
284
  on_io_error(e)
253
- rescue StandardError => e
285
+ rescue Error => e
254
286
  @write_buffer.clear
255
287
  on_error(e)
256
288
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -258,6 +290,10 @@ module HTTPX
258
290
  raise e
259
291
  end
260
292
 
293
+ def initial_call
294
+ call
295
+ end
296
+
261
297
  def close
262
298
  transition(:active) if @state == :inactive
263
299
 
@@ -314,6 +350,11 @@ module HTTPX
314
350
  # (example: HTTP/1 parser disabling pipelining)
315
351
  return if @state == :idle && @pending.any?
316
352
 
353
+ if @ping_timer
354
+ @ping_timer.cancel
355
+ @ping_timer = nil
356
+ end
357
+
317
358
  parser = @parser
318
359
 
319
360
  if parser && parser.respond_to?(:max_concurrent_requests)
@@ -324,7 +365,6 @@ module HTTPX
324
365
  end
325
366
 
326
367
  transition(:closing)
327
-
328
368
  transition(:closed)
329
369
  end
330
370
 
@@ -433,6 +473,11 @@ module HTTPX
433
473
 
434
474
  def on_io_error(e)
435
475
  on_error(e)
476
+
477
+ # do not force close if parser resets the connection.
478
+ # can happen i.e. when HTTP/1.1 pipelining is disabled.
479
+ return if @state == :idle && @pending.any?
480
+
436
481
  force_close(true)
437
482
  end
438
483
 
@@ -456,7 +501,7 @@ module HTTPX
456
501
  reset
457
502
  end
458
503
 
459
- # :nocov:
504
+ # simplecov:disable
460
505
  def inspect
461
506
  "#<#{self.class}:#{object_id} " \
462
507
  "@origin=#{@origin} " \
@@ -464,7 +509,7 @@ module HTTPX
464
509
  "@pending=#{@pending.size} " \
465
510
  "@io=#{@io}>"
466
511
  end
467
- # :nocov:
512
+ # simplecov:enable
468
513
 
469
514
  private
470
515
 
@@ -490,7 +535,7 @@ module HTTPX
490
535
  # * the number of pending requests
491
536
  # * the number of inflight requests
492
537
  # * whether the write buffer has bytes (i.e. for close handshake)
493
- if @pending.empty? && @inflight.zero? && @write_buffer.empty?
538
+ if no_more_requests? && @write_buffer.empty?
494
539
  no_more_requests_loop_check if @parser && @parser.pending.any?
495
540
 
496
541
  # terminate if an altsvc connection has been established
@@ -540,7 +585,7 @@ module HTTPX
540
585
  break if @state == :closing || @state == :closed
541
586
 
542
587
  # exit #consume altogether if all outstanding requests have been dealt with
543
- if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
588
+ if no_more_requests? && @write_buffer.empty? # rubocop:disable Style/Next
544
589
  no_more_requests_loop_check if @parser && @parser.pending.any?
545
590
 
546
591
  # terminate if an altsvc connection has been established
@@ -753,14 +798,19 @@ module HTTPX
753
798
  Errno::ECONNRESET,
754
799
  Errno::EADDRNOTAVAIL,
755
800
  Errno::EHOSTUNREACH,
756
- Errno::EINVAL,
757
801
  Errno::ENETUNREACH,
802
+ Errno::EHOSTDOWN,
803
+ Errno::ENETDOWN,
804
+ Errno::EINVAL,
758
805
  Errno::EPIPE,
759
806
  Errno::ENOENT,
760
807
  SocketError,
761
808
  IOError => e
762
809
  on_connect_error(e)
763
- rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
810
+ rescue TLSError,
811
+ Errno::ETIMEDOUT,
812
+ ::HTTP2::Error::ProtocolError,
813
+ ::HTTP2::Error::HandshakeError => e
764
814
  # connect errors, exit gracefully
765
815
  handle_error(e)
766
816
  handle_connect_error(e) if connecting?
@@ -957,9 +1007,12 @@ module HTTPX
957
1007
 
958
1008
  parser.ping
959
1009
 
1010
+ return unless parser.waiting_for_ping?
1011
+
960
1012
  ping_timeout = @options.timeout[:ping_timeout]
961
1013
 
962
1014
  @ping_timer = @current_selector.after(ping_timeout) do
1015
+ log(level: 3) { "ping timeout expired..." }
963
1016
  error = PingTimeoutError.new(ping_timeout, "Timed out after #{ping_timeout} seconds")
964
1017
  on_error(error)
965
1018
  end
@@ -986,6 +1039,11 @@ module HTTPX
986
1039
  "along with debug logs"
987
1040
  end
988
1041
 
1042
+ # true when there are no more pending nor inflight (in parser) requests
1043
+ def no_more_requests?
1044
+ @pending.empty? && @inflight.zero?
1045
+ end
1046
+
989
1047
  # recover internal state and emit all relevant error responses when +error+ was raised.
990
1048
  # this takes an optiona +request+ which may have already been handled and can be opted out
991
1049
  # in the state recovery process.
@@ -1099,13 +1157,14 @@ module HTTPX
1099
1157
  end
1100
1158
 
1101
1159
  timer = selector.after(timeout, callback)
1102
- request.active_timeouts << label
1160
+ timer.label = label
1161
+ request.active_timeouts << timer
1103
1162
 
1104
1163
  Array(finish_events).each do |event|
1105
1164
  # clean up request timeouts if the connection errors out
1106
1165
  request.set_timeout_callback(event) do
1107
1166
  timer.cancel
1108
- request.active_timeouts.delete(label)
1167
+ request.active_timeouts.delete(timer)
1109
1168
  end
1110
1169
  end
1111
1170
  end
data/lib/httpx/errors.rb CHANGED
@@ -43,6 +43,9 @@ module HTTPX
43
43
  # The HTTPX::Request request object this exception refers to.
44
44
  attr_reader :request
45
45
 
46
+ # The response object this exception refers to.
47
+ attr_reader :response
48
+
46
49
  # initializes the exception with the +request+ and +response+ it refers to, and the
47
50
  # +timeout+ causing the error, and the
48
51
  def initialize(request, response, timeout)
data/lib/httpx/headers.rb CHANGED
@@ -140,12 +140,12 @@ module HTTPX
140
140
  @headers.to_s
141
141
  end
142
142
 
143
- # :nocov:
143
+ # simplecov:disable
144
144
  def inspect
145
145
  "#<#{self.class}:#{object_id} " \
146
146
  "#{to_hash.inspect}>"
147
147
  end
148
- # :nocov:
148
+ # simplecov:enable
149
149
 
150
150
  # this is internal API and doesn't abide to other public API
151
151
  # guarantees, like downcasing strings.