httpx 1.4.0 → 1.4.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 (42) 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/lib/httpx/adapters/datadog.rb +55 -83
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/webmock.rb +7 -1
  8. data/lib/httpx/callbacks.rb +2 -2
  9. data/lib/httpx/connection/http2.rb +2 -2
  10. data/lib/httpx/connection.rb +106 -51
  11. data/lib/httpx/errors.rb +3 -0
  12. data/lib/httpx/loggable.rb +8 -1
  13. data/lib/httpx/plugins/callbacks.rb +1 -0
  14. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  15. data/lib/httpx/plugins/expect.rb +1 -1
  16. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  17. data/lib/httpx/request/body.rb +9 -14
  18. data/lib/httpx/request.rb +20 -0
  19. data/lib/httpx/resolver/https.rb +4 -2
  20. data/lib/httpx/resolver/native.rb +111 -55
  21. data/lib/httpx/resolver/resolver.rb +18 -11
  22. data/lib/httpx/resolver/system.rb +3 -5
  23. data/lib/httpx/selector.rb +33 -23
  24. data/lib/httpx/session.rb +17 -43
  25. data/lib/httpx/timers.rb +16 -1
  26. data/lib/httpx/transcoder/body.rb +15 -31
  27. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  28. data/lib/httpx/version.rb +1 -1
  29. data/lib/httpx.rb +1 -1
  30. data/sig/callbacks.rbs +2 -2
  31. data/sig/connection.rbs +19 -5
  32. data/sig/errors.rbs +3 -0
  33. data/sig/request/body.rbs +0 -8
  34. data/sig/request.rbs +3 -0
  35. data/sig/resolver/native.rbs +6 -1
  36. data/sig/selector.rbs +1 -0
  37. data/sig/session.rbs +2 -0
  38. data/sig/timers.rbs +15 -4
  39. data/sig/transcoder/body.rbs +1 -3
  40. data/sig/transcoder/utils/body_reader.rbs +1 -1
  41. data/sig/transcoder/utils/deflater.rbs +1 -1
  42. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7edc783221b5d919a1c788bf360f0cc2d43ee1556eb893c6b5d377d1bd3b4241
4
- data.tar.gz: 14dc4593ae5c46acb33a4f242a0058e75eddec70bd22fdf75c05496240de3abc
3
+ metadata.gz: 3be0d06eb9b669cc4a1ba0818d39c76bdd4cdf41e496cb82efbb83b7c6c47241
4
+ data.tar.gz: bf778691f222095e080c83b16f71b3d9c8065db9f98f35498eeea317f013a686
5
5
  SHA512:
6
- metadata.gz: 181bc9e6155708c2d1cdaee3dc7c5c15cd5e2b1ba8d7911b6dd5a79e1cebe38558766ec89d8446483d434c7387e330874a70633b2ed5fb8f6a9d8a3401a4abf4
7
- data.tar.gz: 49f57c406a2a5c5be83c018c2151ef2e842c266e93d9609dff15684e662bb25580e7c84e2e96a32d1a45094c9baf8ad18f89dbab1e8235127f4d40429f733572
6
+ metadata.gz: 26045db79f85f7a136771bdca1852d93be6ad9c84a4285d26c7a97543e0209588fdc423276ea593c9ad26db960ec81a444326a01223e24a932cca8318f08e4ab
7
+ data.tar.gz: c280576f30f55590336c70c93ea63a4be1e9a8ac238318417b9c4227889518c04f2545d70c0dce74c4026465ad9a7e68f0a05ac6d9728a1035e8ba5c094036fc
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.
@@ -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
@@ -59,7 +59,9 @@ module WebMock
59
59
 
60
60
  connection.once(:unmock_connection) do
61
61
  unless connection.addresses
62
- connection.__send__(:callbacks)[:connect_error].clear
62
+ # reset Happy Eyeballs, fail early
63
+ connection.sibling = nil
64
+
63
65
  deselect_connection(connection, selector)
64
66
  end
65
67
  resolve_connection(connection, selector)
@@ -114,6 +116,10 @@ module WebMock
114
116
  response = Plugin.build_from_webmock_response(request, mock_response)
115
117
  WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
116
118
  log { "mocking #{request.uri} with #{mock_response.inspect}" }
119
+ request.transition(:headers)
120
+ request.transition(:body)
121
+ request.transition(:trailers)
122
+ request.transition(:done)
117
123
  request.response = response
118
124
  request.emit(:response, response)
119
125
  response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
@@ -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
 
@@ -125,7 +125,7 @@ module HTTPX
125
125
  end
126
126
 
127
127
  def handle_error(ex, request = nil)
128
- if ex.instance_of?(TimeoutError) && !@handshake_completed && @connection.state != :closed
128
+ if ex.is_a?(OperationTimeoutError) && !@handshake_completed && @connection.state != :closed
129
129
  @connection.goaway(:settings_timeout, "closing due to settings timeout")
130
130
  emit(:close_handshake)
131
131
  settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
@@ -137,7 +137,7 @@ module HTTPX
137
137
 
138
138
  emit(:error, req, ex)
139
139
  end
140
- @pending.each do |req|
140
+ while (req = @pending.shift)
141
141
  next if request && request == req
142
142
 
143
143
  emit(:error, req, ex)