httpx 1.4.0 → 1.4.4

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/doc/release_notes/1_4_2.md +20 -0
  5. data/doc/release_notes/1_4_3.md +11 -0
  6. data/doc/release_notes/1_4_4.md +14 -0
  7. data/lib/httpx/adapters/datadog.rb +55 -83
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +11 -1
  10. data/lib/httpx/callbacks.rb +2 -2
  11. data/lib/httpx/connection/http2.rb +33 -18
  12. data/lib/httpx/connection.rb +115 -55
  13. data/lib/httpx/errors.rb +3 -4
  14. data/lib/httpx/io/ssl.rb +6 -3
  15. data/lib/httpx/loggable.rb +13 -6
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  18. data/lib/httpx/plugins/expect.rb +1 -1
  19. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  20. data/lib/httpx/plugins/internal_telemetry.rb +21 -1
  21. data/lib/httpx/plugins/retries.rb +2 -2
  22. data/lib/httpx/plugins/stream.rb +42 -18
  23. data/lib/httpx/request/body.rb +9 -14
  24. data/lib/httpx/request.rb +37 -3
  25. data/lib/httpx/resolver/https.rb +4 -2
  26. data/lib/httpx/resolver/native.rb +111 -55
  27. data/lib/httpx/resolver/resolver.rb +18 -11
  28. data/lib/httpx/resolver/system.rb +3 -5
  29. data/lib/httpx/response.rb +9 -4
  30. data/lib/httpx/selector.rb +33 -23
  31. data/lib/httpx/session.rb +20 -49
  32. data/lib/httpx/timers.rb +16 -1
  33. data/lib/httpx/transcoder/body.rb +15 -31
  34. data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
  35. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  36. data/lib/httpx/version.rb +1 -1
  37. data/lib/httpx.rb +1 -1
  38. data/sig/callbacks.rbs +2 -2
  39. data/sig/connection/http2.rbs +4 -0
  40. data/sig/connection.rbs +19 -5
  41. data/sig/errors.rbs +3 -3
  42. data/sig/loggable.rbs +2 -2
  43. data/sig/plugins/stream.rbs +3 -0
  44. data/sig/pool.rbs +2 -0
  45. data/sig/request/body.rbs +0 -8
  46. data/sig/request.rbs +12 -0
  47. data/sig/resolver/native.rbs +6 -1
  48. data/sig/response.rbs +8 -3
  49. data/sig/selector.rbs +1 -0
  50. data/sig/session.rbs +2 -0
  51. data/sig/timers.rbs +15 -4
  52. data/sig/transcoder/body.rbs +1 -3
  53. data/sig/transcoder/json.rbs +1 -1
  54. data/sig/transcoder/multipart.rbs +1 -1
  55. data/sig/transcoder/utils/body_reader.rbs +1 -1
  56. data/sig/transcoder/utils/deflater.rbs +1 -2
  57. metadata +11 -9
  58. data/lib/httpx/session2.rb +0 -23
  59. data/lib/httpx/transcoder/utils/inflater.rb +0 -21
  60. data/sig/transcoder/utils/inflater.rbs +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7edc783221b5d919a1c788bf360f0cc2d43ee1556eb893c6b5d377d1bd3b4241
4
- data.tar.gz: 14dc4593ae5c46acb33a4f242a0058e75eddec70bd22fdf75c05496240de3abc
3
+ metadata.gz: 4aa5b1cfd43d05f50fc7246219d6a90cb5591e11c8bfeee1f1cf62ffa91993fc
4
+ data.tar.gz: 58f96325ac13b8caddb708464a75ee6dafcfe7b8284f0825803ac3d0c4d8ccc1
5
5
  SHA512:
6
- metadata.gz: 181bc9e6155708c2d1cdaee3dc7c5c15cd5e2b1ba8d7911b6dd5a79e1cebe38558766ec89d8446483d434c7387e330874a70633b2ed5fb8f6a9d8a3401a4abf4
7
- data.tar.gz: 49f57c406a2a5c5be83c018c2151ef2e842c266e93d9609dff15684e662bb25580e7c84e2e96a32d1a45094c9baf8ad18f89dbab1e8235127f4d40429f733572
6
+ metadata.gz: 43a032f78a86df04428af78f32077483b8c01500ebb001ec97100ac3923b4915a28b299cadb8b47fab8fa875c3897732c21eec63769ad98dfd98b901f006b051
7
+ data.tar.gz: dfff14a83428e2472ff53a50cadf5dd7a270f09523eca4c5aa66e7a3093ca8bd096c9c3d12070ed6a54ef1ddaec4966efd456653e666429f0ce47f04fe68e35a
data/README.md CHANGED
@@ -157,7 +157,6 @@ All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
157
157
 
158
158
  * Discuss your contribution in an issue
159
159
  * Fork it
160
- * Make your changes, add some tests
161
- * Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
160
+ * Make your changes, add some tests (follow the instructions from [here](test/README.md))
162
161
  * Open a Merge Request (that's Pull Request in Github-ish)
163
162
  * Wait for feedback
@@ -0,0 +1,19 @@
1
+ # 1.4.1
2
+
3
+ ## Bugfixes
4
+
5
+ * several `datadog` integration bugfixes
6
+ * only load the `datadog` integration when the `datadog` sdk is loaded (and not other gems that may define the `Datadog` module, like `dogstatsd`)
7
+ * do not trace if datadog integration is loaded but disabled
8
+ * distributed headers are now sent along (when the configuration is enabled, which it is by default)
9
+ * fix for handling multiple `GOAWAY` frames coming from the server (node.js servers seem to send multiple frames on connection timeout)
10
+ * fix regression for when a url is used with `httpx` which is not `http://` or `https://` (should raise `HTTPX::UnsupportedSchemaError`)
11
+ * worked around `IO.copy_stream` which was emitting incorrect bytes for HTTP/2 requests which bodies larger than the maximum supported frame size.
12
+ * multipart requests: make sure that a body declared as `Pathname` is opened for reading in binary mode.
13
+ * `webmock` integration: ensure that request events are emitted (such as plugins and integrations relying in it, such as `datadog` and the OTel integration)
14
+ * native resolver: do not propagate successful name resolutions for connections which were already closed.
15
+ * native resolver: fixed name resolution stalling, in a multi-request to multi-origin scenario, when a resolution timeout would happen.
16
+
17
+ ## Chore
18
+
19
+ * refactor of the happy eyeballs and connection coalescing logic to not rely on callbacks, and instead on instance variable management (makes code more straightforward to read).
@@ -0,0 +1,20 @@
1
+ # 1.4.2
2
+
3
+ ## Bugfixes
4
+
5
+ * faraday: use default reason when none is matched by Net::HTTP::STATUS_CODES
6
+ * native resolver: keep sending DNS queries if the socket is available, to avoid busy loops on select
7
+ * native resolver fixes for Happy Eyeballs v2
8
+ * do not apply resolution delay if the IPv4 IP was not resolved via DNS
9
+ * ignore ALIAS if DNS response carries IP answers
10
+ * do not try to query for names already awaiting answer from the resolver
11
+ * make sure all types of errors are propagated to connections
12
+ * make sure next candidate is picked up if receiving NX_DOMAIN_NOT_FOUND error from resolver
13
+ * raise error happening before any request is flushed to respective connections (avoids loop on non-actionable selector termination).
14
+ * fix "NoMethodError: undefined method `after' for nil:NilClass", happening for requests flushed into persistent connections which errored, and were retried in a different connection before triggering the timeout callbacks from the previously-closed connection.
15
+
16
+
17
+ ## Chore
18
+
19
+ * Refactor of timers to allow for explicit and more performant single timer interval cancellation.
20
+ * default log message restructured to include info about process, thread and caller.
@@ -0,0 +1,11 @@
1
+ # 1.4.3
2
+
3
+ ## Bugfixes
4
+
5
+ * `webmock` adapter: reassign headers to signature after callbacks are called (these may change the headers before virtual send).
6
+ * do not close request (and its body) right after sending, instead only on response close
7
+ * prevents retries from failing under the `:retries` plugin
8
+ * fixes issue when using `faraday-multipart` request bodies
9
+ * retry request with HTTP/1 when receiving an HTTP/2 GOAWAY frame with `HTTP_1_1_REQUIRED` error code.
10
+ * fix wrong method call on HTTP/2 PING frame with unrecognized code.
11
+ * fix EOFError issues on connection termination for long running connections which may have already been terminated by peer and were wrongly trying to complete the HTTP/2 termination handshake.
@@ -0,0 +1,14 @@
1
+ # 1.4.4
2
+
3
+ ## Improvements
4
+
5
+ * `:stream` plugin: response will now be partially buffered in order to i.e. inspect response status or headers on the response body without buffering the full response
6
+ * this fixes an issue in the `down` gem integration when used with the `:max_size` option.
7
+ * do not unnecessarily probe for connection liveness if no more requests are inflight, including failed ones.
8
+ * when using persistent connections, do not probe for liveness right after reconnecting after a keep alive timeout.
9
+
10
+ ## Bugfixes
11
+
12
+ * `:persistent` plugin: do not exhaust retry attempts when probing for (and failing) connection liveness.
13
+ * since the introduction of per-session connection pools, and consequentially due to the possibility of multiple inactive connections for the same origin being in the pool, which may have been terminated by the peer server, requests would fail before being able to establish a new connection.
14
+ * prevent retrying to connect the TCP socket object when an SSLSocket object is already in place and connecting.
@@ -27,62 +27,54 @@ module Datadog::Tracing
27
27
  # Enables tracing for httpx requests.
28
28
  #
29
29
  # A span will be created for each request transaction; the span is created lazily only when
30
- # receiving a response, and it is fed the start time stored inside the tracer object.
30
+ # buffering a request, and it is fed the start time stored inside the tracer object.
31
31
  #
32
32
  module Plugin
33
- class RequestTracer
34
- include Contrib::HttpAnnotationHelper
33
+ module RequestTracer
34
+ extend Contrib::HttpAnnotationHelper
35
+
36
+ module_function
35
37
 
36
38
  SPAN_REQUEST = "httpx.request"
37
39
 
38
- # initializes the tracer object on the +request+.
39
- def initialize(request)
40
- @request = request
41
- @start_time = nil
40
+ # initializes tracing on the +request+.
41
+ def call(request)
42
+ return unless configuration(request).enabled
43
+
44
+ span = nil
42
45
 
43
46
  # request objects are reused, when already buffered requests get rerouted to a different
44
47
  # connection due to connection issues, or when they already got a response, but need to
45
48
  # be retried. In such situations, the original span needs to be extended for the former,
46
49
  # while a new is required for the latter.
47
- request.on(:idle) { reset }
50
+ request.on(:idle) do
51
+ span = nil
52
+ end
48
53
  # the span is initialized when the request is buffered in the parser, which is the closest
49
54
  # one gets to actually sending the request.
50
- request.on(:headers) { call }
51
- end
52
-
53
- # sets up the span start time, while preparing the on response callback.
54
- def call(*args)
55
- return if @start_time
56
-
57
- start(*args)
55
+ request.on(:headers) do
56
+ next if span
58
57
 
59
- @request.once(:response, &method(:finish))
60
- end
58
+ span = initialize_span(request, now)
59
+ end
61
60
 
62
- private
61
+ request.on(:response) do |response|
62
+ unless span
63
+ next unless response.is_a?(::HTTPX::ErrorResponse) && response.error.respond_to?(:connection)
63
64
 
64
- # just sets the span init time. It can be passed a +start_time+ in cases where
65
- # this is collected outside the request transaction.
66
- def start(start_time = now)
67
- @start_time = start_time
68
- end
65
+ # handles the case when the +error+ happened during name resolution, which means
66
+ # that the tracing start point hasn't been triggered yet; in such cases, the approximate
67
+ # initial resolving time is collected from the connection, and used as span start time,
68
+ # and the tracing object in inserted before the on response callback is called.
69
+ span = initialize_span(request, response.error.connection.init_time)
69
70
 
70
- # resets the start time for already finished request transactions.
71
- def reset
72
- return unless @start_time
71
+ end
73
72
 
74
- start
73
+ finish(response, span)
74
+ end
75
75
  end
76
76
 
77
- # creates the span from the collected +@start_time+ to what the +response+ state
78
- # contains. It also resets internal state to allow this object to be reused.
79
- def finish(response)
80
- return unless @start_time
81
-
82
- span = initialize_span
83
-
84
- return unless span
85
-
77
+ def finish(response, span)
86
78
  if response.is_a?(::HTTPX::ErrorResponse)
87
79
  span.set_error(response.error)
88
80
  else
@@ -92,40 +84,40 @@ module Datadog::Tracing
92
84
  end
93
85
 
94
86
  span.finish
95
- ensure
96
- @start_time = nil
97
87
  end
98
88
 
99
89
  # return a span initialized with the +@request+ state.
100
- def initialize_span
101
- verb = @request.verb
102
- uri = @request.uri
90
+ def initialize_span(request, start_time)
91
+ verb = request.verb
92
+ uri = request.uri
93
+
94
+ config = configuration(request)
103
95
 
104
- span = create_span(@request)
96
+ span = create_span(request, config, start_time)
105
97
 
106
98
  span.resource = verb
107
99
 
108
100
  # Add additional request specific tags to the span.
109
101
 
110
- span.set_tag(TAG_URL, @request.path)
102
+ span.set_tag(TAG_URL, request.path)
111
103
  span.set_tag(TAG_METHOD, verb)
112
104
 
113
105
  span.set_tag(TAG_TARGET_HOST, uri.host)
114
- span.set_tag(TAG_TARGET_PORT, uri.port.to_s)
106
+ span.set_tag(TAG_TARGET_PORT, uri.port)
115
107
 
116
108
  # Tag as an external peer service
117
109
  span.set_tag(TAG_PEER_SERVICE, span.service)
118
110
 
119
- if configuration[:distributed_tracing]
111
+ if config[:distributed_tracing]
120
112
  propagate_trace_http(
121
- Datadog::Tracing.active_trace.to_digest,
122
- @request.headers
113
+ Datadog::Tracing.active_trace,
114
+ request.headers
123
115
  )
124
116
  end
125
117
 
126
118
  # Set analytics sample rate
127
- if Contrib::Analytics.enabled?(configuration[:analytics_enabled])
128
- Contrib::Analytics.set_sample_rate(span, configuration[:analytics_sample_rate])
119
+ if Contrib::Analytics.enabled?(config[:analytics_enabled])
120
+ Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
129
121
  end
130
122
 
131
123
  span
@@ -138,34 +130,34 @@ module Datadog::Tracing
138
130
  ::Datadog::Core::Utils::Time.now.utc
139
131
  end
140
132
 
141
- def configuration
142
- @configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
133
+ def configuration(request)
134
+ Datadog.configuration.tracing[:httpx, request.uri.host]
143
135
  end
144
136
 
145
137
  if Gem::Version.new(DATADOG_VERSION::STRING) >= Gem::Version.new("2.0.0")
146
- def propagate_trace_http(digest, headers)
147
- Datadog::Tracing::Contrib::HTTP.inject(digest, headers)
138
+ def propagate_trace_http(trace, headers)
139
+ Datadog::Tracing::Contrib::HTTP.inject(trace, headers)
148
140
  end
149
141
 
150
- def create_span(request)
142
+ def create_span(request, configuration, start_time)
151
143
  Datadog::Tracing.trace(
152
144
  SPAN_REQUEST,
153
- service: service_name(request.uri.host, configuration, Datadog.configuration_for(self)),
145
+ service: service_name(request.uri.host, configuration),
154
146
  type: TYPE_OUTBOUND,
155
- start_time: @start_time
147
+ start_time: start_time
156
148
  )
157
149
  end
158
150
  else
159
- def propagate_trace_http(digest, headers)
160
- Datadog::Tracing::Propagation::HTTP.inject!(digest, headers)
151
+ def propagate_trace_http(trace, headers)
152
+ Datadog::Tracing::Propagation::HTTP.inject!(trace.to_digest, headers)
161
153
  end
162
154
 
163
- def create_span(request)
155
+ def create_span(request, configuration, start_time)
164
156
  Datadog::Tracing.trace(
165
157
  SPAN_REQUEST,
166
- service: service_name(request.uri.host, configuration, Datadog.configuration_for(self)),
158
+ service: service_name(request.uri.host, configuration),
167
159
  span_type: TYPE_OUTBOUND,
168
- start_time: @start_time
160
+ start_time: start_time
169
161
  )
170
162
  end
171
163
  end
@@ -178,7 +170,7 @@ module Datadog::Tracing
178
170
 
179
171
  return unless Datadog::Tracing.enabled?
180
172
 
181
- RequestTracer.new(self)
173
+ RequestTracer.call(self)
182
174
  end
183
175
  end
184
176
 
@@ -190,26 +182,6 @@ module Datadog::Tracing
190
182
 
191
183
  @init_time = ::Datadog::Core::Utils::Time.now.utc
192
184
  end
193
-
194
- # handles the case when the +error+ happened during name resolution, which meanns
195
- # that the tracing logic hasn't been injected yet; in such cases, the approximate
196
- # initial resolving time is collected from the connection, and used as span start time,
197
- # and the tracing object in inserted before the on response callback is called.
198
- def handle_error(error, request = nil)
199
- return super unless Datadog::Tracing.enabled?
200
-
201
- return super unless error.respond_to?(:connection)
202
-
203
- @pending.each do |req|
204
- next if request and request == req
205
-
206
- RequestTracer.new(req).call(error.connection.init_time)
207
- end
208
-
209
- RequestTracer.new(request).call(error.connection.init_time) if request
210
-
211
- super
212
- end
213
185
  end
214
186
  end
215
187
 
@@ -149,7 +149,7 @@ module Faraday
149
149
 
150
150
  module ResponseMethods
151
151
  def reason
152
- Net::HTTP::STATUS_CODES.fetch(@status)
152
+ Net::HTTP::STATUS_CODES.fetch(@status, "Non-Standard status code")
153
153
  end
154
154
  end
155
155
  end
@@ -58,8 +58,12 @@ module WebMock
58
58
  super
59
59
 
60
60
  connection.once(:unmock_connection) do
61
+ next unless connection.current_session == self
62
+
61
63
  unless connection.addresses
62
- connection.__send__(:callbacks)[:connect_error].clear
64
+ # reset Happy Eyeballs, fail early
65
+ connection.sibling = nil
66
+
63
67
  deselect_connection(connection, selector)
64
68
  end
65
69
  resolve_connection(connection, selector)
@@ -114,8 +118,14 @@ module WebMock
114
118
  response = Plugin.build_from_webmock_response(request, mock_response)
115
119
  WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
116
120
  log { "mocking #{request.uri} with #{mock_response.inspect}" }
121
+ request.transition(:headers)
122
+ request.transition(:body)
123
+ request.transition(:trailers)
124
+ request.transition(:done)
117
125
  request.response = response
118
126
  request.emit(:response, response)
127
+ request_signature.headers = request.headers.to_h
128
+
119
129
  response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
120
130
  elsif WebMock.net_connect_allowed?(request_signature.uri)
121
131
  if WebMock::CallbackRegistry.any_callbacks?
@@ -4,7 +4,7 @@ module HTTPX
4
4
  module Callbacks
5
5
  def on(type, &action)
6
6
  callbacks(type) << action
7
- self
7
+ action
8
8
  end
9
9
 
10
10
  def once(type, &block)
@@ -12,10 +12,10 @@ module HTTPX
12
12
  block.call(*args, &callback)
13
13
  :delete
14
14
  end
15
- self
16
15
  end
17
16
 
18
17
  def emit(type, *args)
18
+ log { "emit #{type.inspect} callbacks" } if respond_to?(:log)
19
19
  callbacks(type).delete_if { |pr| :delete == pr.call(*args) } # rubocop:disable Style/YodaCondition
20
20
  end
21
21
 
@@ -16,6 +16,12 @@ module HTTPX
16
16
  end
17
17
  end
18
18
 
19
+ class PingError < Error
20
+ def initialize
21
+ super(0, :ping_error)
22
+ end
23
+ end
24
+
19
25
  class GoawayError < Error
20
26
  def initialize
21
27
  super(0, :no_error)
@@ -125,7 +131,7 @@ module HTTPX
125
131
  end
126
132
 
127
133
  def handle_error(ex, request = nil)
128
- if ex.instance_of?(TimeoutError) && !@handshake_completed && @connection.state != :closed
134
+ if ex.is_a?(OperationTimeoutError) && !@handshake_completed && @connection.state != :closed
129
135
  @connection.goaway(:settings_timeout, "closing due to settings timeout")
130
136
  emit(:close_handshake)
131
137
  settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
@@ -137,7 +143,7 @@ module HTTPX
137
143
 
138
144
  emit(:error, req, ex)
139
145
  end
140
- @pending.each do |req|
146
+ while (req = @pending.shift)
141
147
  next if request && request == req
142
148
 
143
149
  emit(:error, req, ex)
@@ -311,17 +317,20 @@ module HTTPX
311
317
  @streams.delete(request)
312
318
 
313
319
  if error
314
- ex = Error.new(stream.id, error)
315
- ex.set_backtrace(caller)
316
- response = ErrorResponse.new(request, ex)
317
- request.response = response
318
- emit(:response, request, response)
320
+ case error
321
+ when :http_1_1_required
322
+ emit(:error, request, error)
323
+ else
324
+ ex = Error.new(stream.id, error)
325
+ ex.set_backtrace(caller)
326
+ response = ErrorResponse.new(request, ex)
327
+ request.response = response
328
+ emit(:response, request, response)
329
+ end
319
330
  else
320
331
  response = request.response
321
332
  if response && response.is_a?(Response) && response.status == 421
322
- ex = MisdirectedRequestError.new(response)
323
- ex.set_backtrace(caller)
324
- emit(:error, request, ex)
333
+ emit(:error, request, :http_1_1_required)
325
334
  else
326
335
  emit(:response, request, response)
327
336
  end
@@ -352,7 +361,12 @@ module HTTPX
352
361
  is_connection_closed = @connection.state == :closed
353
362
  if error
354
363
  @buffer.clear if is_connection_closed
355
- if error == :no_error
364
+ case error
365
+ when :http_1_1_required
366
+ while (request = @pending.shift)
367
+ emit(:error, request, error)
368
+ end
369
+ when :no_error
356
370
  ex = GoawayError.new
357
371
  @pending.unshift(*@streams.keys)
358
372
  @drains.clear
@@ -360,8 +374,11 @@ module HTTPX
360
374
  else
361
375
  ex = Error.new(0, error)
362
376
  end
363
- ex.set_backtrace(caller)
364
- handle_error(ex)
377
+
378
+ if ex
379
+ ex.set_backtrace(caller)
380
+ handle_error(ex)
381
+ end
365
382
  end
366
383
  return unless is_connection_closed && @streams.empty?
367
384
 
@@ -403,11 +420,9 @@ module HTTPX
403
420
  end
404
421
 
405
422
  def on_pong(ping)
406
- if @pings.delete(ping.to_s)
407
- emit(:pong)
408
- else
409
- close(:protocol_error, "ping payload did not match")
410
- end
423
+ raise PingError unless @pings.delete(ping.to_s)
424
+
425
+ emit(:pong)
411
426
  end
412
427
  end
413
428
  end