httpx 0.11.3 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/doc/release_notes/0_11_1.md +5 -1
  4. data/doc/release_notes/0_12_0.md +55 -0
  5. data/lib/httpx.rb +2 -1
  6. data/lib/httpx/adapters/faraday.rb +4 -6
  7. data/lib/httpx/altsvc.rb +1 -0
  8. data/lib/httpx/connection.rb +63 -15
  9. data/lib/httpx/connection/http1.rb +8 -7
  10. data/lib/httpx/connection/http2.rb +32 -25
  11. data/lib/httpx/io.rb +16 -3
  12. data/lib/httpx/io/ssl.rb +7 -9
  13. data/lib/httpx/io/tcp.rb +9 -8
  14. data/lib/httpx/io/tls.rb +218 -0
  15. data/lib/httpx/io/tls/box.rb +365 -0
  16. data/lib/httpx/io/tls/context.rb +199 -0
  17. data/lib/httpx/io/tls/ffi.rb +390 -0
  18. data/lib/httpx/parser/http1.rb +4 -4
  19. data/lib/httpx/plugins/aws_sdk_authentication.rb +81 -0
  20. data/lib/httpx/plugins/aws_sigv4.rb +218 -0
  21. data/lib/httpx/plugins/compression/deflate.rb +2 -5
  22. data/lib/httpx/plugins/internal_telemetry.rb +93 -0
  23. data/lib/httpx/plugins/multipart.rb +2 -0
  24. data/lib/httpx/plugins/multipart/encoder.rb +4 -9
  25. data/lib/httpx/plugins/proxy.rb +1 -1
  26. data/lib/httpx/plugins/proxy/http.rb +1 -1
  27. data/lib/httpx/plugins/proxy/socks4.rb +8 -0
  28. data/lib/httpx/plugins/proxy/socks5.rb +8 -0
  29. data/lib/httpx/plugins/push_promise.rb +3 -2
  30. data/lib/httpx/plugins/retries.rb +1 -1
  31. data/lib/httpx/plugins/stream.rb +3 -5
  32. data/lib/httpx/pool.rb +0 -1
  33. data/lib/httpx/registry.rb +1 -7
  34. data/lib/httpx/request.rb +11 -1
  35. data/lib/httpx/resolver/https.rb +3 -11
  36. data/lib/httpx/response.rb +9 -2
  37. data/lib/httpx/selector.rb +5 -0
  38. data/lib/httpx/session.rb +25 -2
  39. data/lib/httpx/transcoder/body.rb +3 -5
  40. data/lib/httpx/version.rb +1 -1
  41. data/sig/connection/http1.rbs +2 -2
  42. data/sig/connection/http2.rbs +5 -3
  43. data/sig/plugins/aws_sdk_authentication.rbs +17 -0
  44. data/sig/plugins/aws_sigv4.rbs +65 -0
  45. data/sig/plugins/push_promise.rbs +1 -1
  46. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1612c9f4696a7a6ae0508ea66f864654e28470e742b91b57cd14ab5b43adad2
4
- data.tar.gz: 791569d7282f7cb3e451d245e332871c170e4571ca5e10ead757f93afffe3d78
3
+ metadata.gz: 5770fadc8d4604ccb0fb274c7fc157802315f68c749a83973ee5596fee8effb1
4
+ data.tar.gz: 4f24cca053093be31636405dcb740f5c6b2599197d9ebdfb90cbf127e86a5ffc
5
5
  SHA512:
6
- metadata.gz: 96e8bc5d07f59b21ccdbc54bb11bce9b1deb53eb128c4d6c2652a0dfca6c925cbe084e82f352857004420cc5753fbacc3a68e2bc4d885937a2afbfa5c6138836
7
- data.tar.gz: b74cbac2346d110a97e6ab9a13f2a62db7ed23ce1454df510f9c8cb6ea58314edff2724b9cc269cdf8d5fdf00e9ce6c5c28eff864605f6ce01a9e2f4b9078911
6
+ metadata.gz: 48fe64207a82e2b8db91b73cc6252ff48ea624e57dcbcbfa30e5f6986e1936e61bbdc83cece34c470dd6e318f41845a306a57e2b5c8710084444b6193786a1eb
7
+ data.tar.gz: 70b43fc624a8452187a730e39f55d9e92d16438babbd76aa2346c5b3dd0c2cad7f8e5e787f7c4ab1917c52ed750c7656f2f6c5e92a2ec0fce9cc0a76e5994d1e
data/README.md CHANGED
@@ -113,7 +113,7 @@ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/h
113
113
 
114
114
  ## Supported Rubies
115
115
 
116
- All Rubies greater or equal to 2.1, and always latest JRuby.
116
+ All Rubies greater or equal to 2.1, and always latest JRuby and Truffleruby.
117
117
 
118
118
  **Note**: This gem is tested against all latest patch versions, i.e. if you're using 2.2.0 and you experience some issue, please test it against 2.2.10 (latest patch version of 2.2) before creating an issue.
119
119
 
@@ -133,7 +133,7 @@ All Rubies greater or equal to 2.1, and always latest JRuby.
133
133
 
134
134
  If your requirement is to run requests over HTTP/2 and TLS, make sure you run a version of the gem which compiles OpenSSL 1.0.2 (Ruby 2.3 and higher are guaranteed to).
135
135
 
136
- JRuby's `openssl` is based on Bouncy Castle, which is massively outdated and still doesn't implement ALPN. So HTTP/2 over TLS/ALPN negotiation is off until JRuby figures this out.
136
+ In order to use HTTP/2 under JRuby, [check this link](https://gitlab.com/honeyryderchuck/httpx/-/wikis/JRuby-Truffleruby-Other-Rubies) to know what to do.
137
137
 
138
138
  ### Known bugs
139
139
 
@@ -1 +1,5 @@
1
- 0_11_1.md
1
+ # 0.11.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixed a bug related to the `:compression` plugin trying to process a last empty DATA frame from an HTTP/2 response, after it had been closed.
@@ -0,0 +1,55 @@
1
+ # 0.12.0
2
+
3
+ ## Features
4
+
5
+ ### AWS Sigv4 Authentication Plugin
6
+
7
+ A new plugin, `:aws_sigv4`, is now shipped with `httpx`. It implements the [AWS Signature Version 4 request signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html), a well documented way of authenticating requests to AWS services, which has since been adopted by other cloud providers, such as Google Cloud Storage.
8
+
9
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#sessionaws_sigv4_authentication
10
+
11
+ For convenience, there's a derivative plugin, `:aws_sdk_authentication`, which builds on top of `:aws_sigv4`, and integrates with the `aws-sdk-core` gem, maintained by AWS, to resolve the authentication credentials (p.ex. if you support ephemeral access keys).
12
+
13
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#sessionaws_sdk_authentication
14
+
15
+ Other FAQ: https://gitlab.com/honeyryderchuck/httpx/-/wikis/AWS-Sigv4#faqs
16
+
17
+ ### HTTP/2 support for JRuby
18
+
19
+ `jruby-openssl` doesn't support ALPN protocol negotiation, nor are there plans to implement, which limited the seamless HTTP/2 usage in `httpx`. A new connection adapter was therefore added specifically for JRuby, where ssl/tls connections will be handled using ffi-based openssl bindings, provided you bundle `ffi-compiler` and `concurrent-ruby`, and install a TLS/1.2-compatible `openssl` package.
20
+
21
+ See how to use it here: https://gitlab.com/honeyryderchuck/httpx/-/wikis/JRuby-Truffleruby-Other-Rubies#http2
22
+
23
+ ## Improvements
24
+
25
+
26
+ ### truffleruby support
27
+
28
+ `httpx` supports and tests against `truffleruby` (known to run tests since v20.3, passing all tests since v21).
29
+
30
+ ### Performance
31
+
32
+ Several optimizations were introduced:
33
+
34
+ * Reduction in read/write system calls;
35
+ * more usage of `String#byteslice` in parsing (instead of string mutation);
36
+ * Avoid selection on connections with no outstanding requests;
37
+
38
+ They all contributed to a massive performance improvement, itself reflected in test runs, which need half the time they used to to complete.
39
+
40
+ ### APIs
41
+
42
+ * `HTTPX::ErrorResponse#to_s` now uses the exception full message, instead of just the backtrace.
43
+
44
+ ## Bugfixes
45
+
46
+ * HTTP/2 stream protocol errors do not cause the process to hang (instead, error responnses are yielded);
47
+ * Fixed body stream bugs on retries when error causing retry would happen mid-transfer;
48
+ * Fixed `:multipart` plugin body rewind on retries to start the transfer from the beginning;
49
+ * Fixed auto-load of `:proxy` plugin when `HTTPS_PROXY` or `HTTP_PROXY` is set;
50
+ * Errno::EPIPE errors mid transfer now cause `httpx` to read from the server and get the appropriate HTTP error response;
51
+ * Make sure that all requests have an error responnse if the error happens early;
52
+ * Fixed TCP handshake Errno::INPROGRESS handling inside TLS connnections, which was causing the process to hang in a high handshake contention scenario;
53
+ * Do not call the event loop if there's nothing to listen on (the DoH resolver was being listened on even if there was nothing to be request);
54
+ * Fixed double event registry for DoH resolvers;
55
+ *
data/lib/httpx.rb CHANGED
@@ -19,7 +19,6 @@ require "httpx/headers"
19
19
  require "httpx/request"
20
20
  require "httpx/response"
21
21
  require "httpx/chainable"
22
- require "httpx/session"
23
22
 
24
23
  # Top-Level Namespace
25
24
  #
@@ -59,3 +58,5 @@ module HTTPX
59
58
 
60
59
  extend Chainable
61
60
  end
61
+
62
+ require "httpx/session"
@@ -123,11 +123,9 @@ module Faraday
123
123
  end
124
124
 
125
125
  def method_missing(meth, *args, &blk)
126
- if @env && @env.respond_to?(meth)
127
- @env.__send__(meth, *args, &blk)
128
- else
129
- super
130
- end
126
+ return super unless @env && @env.respond_to?(meth)
127
+
128
+ @env.__send__(meth, *args, &blk)
131
129
  end
132
130
  end
133
131
 
@@ -197,7 +195,7 @@ module Faraday
197
195
  response_headers.merge!(response.headers)
198
196
  end
199
197
  @app.call(env)
200
- rescue OpenSSL::SSL::SSLError => e
198
+ rescue ::HTTPX::TLSError => e
201
199
  raise SSL_ERROR, e
202
200
  rescue Errno::ECONNABORTED,
203
201
  Errno::ECONNREFUSED,
data/lib/httpx/altsvc.rb CHANGED
@@ -37,6 +37,7 @@ module HTTPX
37
37
  end
38
38
 
39
39
  def emit(request, response)
40
+ return unless response.respond_to?(:headers)
40
41
  # Alt-Svc
41
42
  return unless response.headers.key?("alt-svc")
42
43
 
@@ -170,7 +170,7 @@ module HTTPX
170
170
  end
171
171
 
172
172
  # if the write buffer is full, we drain it
173
- return :w if @write_buffer.full?
173
+ return :w unless @write_buffer.empty?
174
174
 
175
175
  return @parser.interests if @parser
176
176
 
@@ -251,11 +251,18 @@ module HTTPX
251
251
 
252
252
  def consume
253
253
  catch(:called) do
254
+ epiped = false
254
255
  loop do
255
256
  parser.consume
256
257
 
257
- # we exit if there's no more data to process
258
- if @pending.size.zero? && @inflight.zero?
258
+ # we exit if there's no more requests to process
259
+ #
260
+ # this condition takes into account:
261
+ #
262
+ # * the number of inflight requests
263
+ # * the number of pending requests
264
+ # * whether the write buffer has bytes (i.e. for close handshake)
265
+ if @pending.size.zero? && @inflight.zero? && @write_buffer.empty?
259
266
  log(level: 3) { "NO MORE REQUESTS..." }
260
267
  return
261
268
  end
@@ -265,9 +272,17 @@ module HTTPX
265
272
  read_drained = false
266
273
  write_drained = nil
267
274
 
268
- # dread
275
+ #
276
+ # tight read loop.
277
+ #
278
+ # read as much of the socket as possible.
279
+ #
280
+ # this tight loop reads all the data it can from the socket and pipes it to
281
+ # its parser.
282
+ #
269
283
  loop do
270
284
  siz = @io.read(@window_size, @read_buffer)
285
+ log(level: 3, color: :cyan) { "IO READ: #{siz} bytes..." }
271
286
  unless siz
272
287
  ex = EOFError.new("descriptor closed")
273
288
  ex.set_backtrace(caller)
@@ -275,27 +290,53 @@ module HTTPX
275
290
  return
276
291
  end
277
292
 
293
+ # socket has been drained. mark and exit the read loop.
278
294
  if siz.zero?
279
295
  read_drained = @read_buffer.empty?
296
+ epiped = false
280
297
  break
281
298
  end
282
299
 
283
300
  parser << @read_buffer.to_s
284
301
 
302
+ # continue reading if possible.
303
+ break if interests == :w && !epiped
304
+
305
+ # exit the read loop if connection is preparing to be closed
285
306
  break if @state == :closing || @state == :closed
286
307
 
287
- # for HTTP/2, we just want to write goaway frame
288
- end unless @state == :closing
308
+ # exit #consume altogether if all outstanding requests have been dealt with
309
+ return if @pending.size.zero? && @inflight.zero?
310
+ end unless (interests == :w || @state == :closing) && !epiped
289
311
 
290
- # dwrite
312
+ #
313
+ # tight write loop.
314
+ #
315
+ # flush as many bytes as the sockets allow.
316
+ #
291
317
  loop do
318
+ # buffer has been drainned, mark and exit the write loop.
292
319
  if @write_buffer.empty?
293
320
  # we only mark as drained on the first loop
294
321
  write_drained = write_drained.nil? && @inflight.positive?
322
+
295
323
  break
296
324
  end
297
325
 
298
- siz = @io.write(@write_buffer)
326
+ begin
327
+ siz = @io.write(@write_buffer)
328
+ rescue Errno::EPIPE
329
+ # this can happen if we still have bytes in the buffer to send to the server, but
330
+ # the server wants to respond immediately with some message, or an error. An example is
331
+ # when one's uploading a big file to an unintended endpoint, and the server stops the
332
+ # consumption, and responds immediately with an authorization of even method not allowed error.
333
+ # at this point, we have to let the connection switch to read-mode.
334
+ log(level: 2) { "pipe broken, could not flush buffer..." }
335
+ epiped = true
336
+ read_drained = false
337
+ break
338
+ end
339
+ log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
299
340
  unless siz
300
341
  ex = EOFError.new("descriptor closed")
301
342
  ex.set_backtrace(caller)
@@ -303,21 +344,28 @@ module HTTPX
303
344
  return
304
345
  end
305
346
 
347
+ # socket closed for writing. mark and exit the write loop.
306
348
  if siz.zero?
307
349
  write_drained = !@write_buffer.empty?
308
350
  break
309
351
  end
310
352
 
311
- break if @state == :closing || @state == :closed
353
+ # exit write loop if marked to consume from peer, or is closing.
354
+ break if interests == :r || @state == :closing || @state == :closed
312
355
 
313
356
  write_drained = false
314
- end
357
+ end unless interests == :r
315
358
 
316
359
  # return if socket is drained
317
- if read_drained && write_drained
318
- log(level: 3) { "WAITING FOR EVENTS..." }
319
- return
320
- end
360
+ next unless (interests != :r || read_drained) &&
361
+ (interests != :w || write_drained)
362
+
363
+ # gotta go back to the event loop. It happens when:
364
+ #
365
+ # * the socket is drained of bytes or it's not the interest of the conn to read;
366
+ # * theres nothing more to write, or it's not in the interest of the conn to write;
367
+ log(level: 3) { "(#{interests}): WAITING FOR EVENTS..." }
368
+ return
321
369
  end
322
370
  end
323
371
  end
@@ -444,7 +492,7 @@ module HTTPX
444
492
  throw(:jump_tick)
445
493
  rescue Errno::ECONNREFUSED,
446
494
  Errno::EADDRNOTAVAIL,
447
- OpenSSL::SSL::SSLError => e
495
+ TLSError => e
448
496
  # connect errors, exit gracefully
449
497
  handle_error(e)
450
498
  @state = :closed
@@ -69,14 +69,16 @@ module HTTPX
69
69
 
70
70
  return if @requests.include?(request)
71
71
 
72
+ request.once(:headers, &method(:set_protocol_headers))
72
73
  @requests << request
73
74
  @pipelining = true if @requests.size > 1
74
75
  end
75
76
 
76
77
  def consume
77
- requests_limit = [@max_concurrent_requests, @max_requests, @requests.size].min
78
+ requests_limit = [@max_requests, @requests.size].min
79
+ concurrent_requests_limit = [@max_concurrent_requests, requests_limit].min
78
80
  @requests.each_with_index do |request, idx|
79
- break if idx >= requests_limit
81
+ break if idx >= concurrent_requests_limit
80
82
  next if request.state == :done
81
83
 
82
84
  request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
@@ -115,7 +117,7 @@ module HTTPX
115
117
  response = @request.response
116
118
  log(level: 2) { "trailer headers received" }
117
119
 
118
- log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
120
+ log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v.join(", ")}" }.join("\n") }
119
121
  response.merge_headers(h)
120
122
  end
121
123
 
@@ -161,13 +163,13 @@ module HTTPX
161
163
  end
162
164
 
163
165
  def handle_error(ex)
164
- if ex.is_a?(EOFError) && @request && @request.response &&
166
+ if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request && @request.response &&
165
167
  !@request.response.headers.key?("content-length") &&
166
168
  !@request.response.headers.key?("transfer-encoding")
167
169
  # if the response does not contain a content-length header, the server closing the
168
170
  # connnection is the indicator of response consumed.
169
171
  # https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.4
170
- on_complete
172
+ catch(:called) { on_complete }
171
173
  return
172
174
  end
173
175
 
@@ -250,7 +252,7 @@ module HTTPX
250
252
  @pipelining = false
251
253
  end
252
254
 
253
- def set_request_headers(request)
255
+ def set_protocol_headers(request)
254
256
  request.headers["host"] ||= request.authority
255
257
  request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
256
258
  if !request.headers.key?("content-length") &&
@@ -264,7 +266,6 @@ module HTTPX
264
266
  end
265
267
 
266
268
  def handle(request)
267
- set_request_headers(request)
268
269
  catch(:buffer_full) do
269
270
  request.transition(:headers)
270
271
  join_headers(request) if request.state == :headers
@@ -42,11 +42,15 @@ module HTTPX
42
42
  return @buffer.empty? ? :r : :rw
43
43
  end
44
44
 
45
- return :w unless @pending.empty?
45
+ return :w if !@pending.empty? && can_buffer_more_requests?
46
46
 
47
47
  return :w if @streams.each_key.any? { |r| r.interests == :w }
48
48
 
49
- return :r if @buffer.empty?
49
+ if @buffer.empty?
50
+ return if @streams.empty? && @pings.empty?
51
+
52
+ return :r
53
+ end
50
54
 
51
55
  :rw
52
56
  end
@@ -70,10 +74,14 @@ module HTTPX
70
74
  @connection << data
71
75
  end
72
76
 
77
+ def can_buffer_more_requests?
78
+ @handshake_completed &&
79
+ @streams.size < @max_concurrent_requests &&
80
+ @streams.size < @max_requests
81
+ end
82
+
73
83
  def send(request)
74
- if !@handshake_completed ||
75
- @streams.size >= @max_concurrent_requests ||
76
- @streams.size >= @max_requests
84
+ unless can_buffer_more_requests?
77
85
  @pending << request
78
86
  return
79
87
  end
@@ -83,6 +91,7 @@ module HTTPX
83
91
  @streams[request] = stream
84
92
  @max_requests -= 1
85
93
  end
94
+ request.once(:headers, &method(:set_protocol_headers))
86
95
  handle(request, stream)
87
96
  true
88
97
  rescue HTTP2Next::Error::StreamLimitExceeded
@@ -126,8 +135,6 @@ module HTTPX
126
135
  request.path
127
136
  end
128
137
 
129
- def set_request_headers(request); end
130
-
131
138
  def handle(request, stream)
132
139
  catch(:buffer_full) do
133
140
  request.transition(:headers)
@@ -172,18 +179,18 @@ module HTTPX
172
179
  stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
173
180
  end
174
181
 
182
+ def set_protocol_headers(request)
183
+ request.headers[":scheme"] = request.scheme
184
+ request.headers[":method"] = request.verb.to_s.upcase
185
+ request.headers[":path"] = headline_uri(request)
186
+ request.headers[":authority"] = request.authority
187
+ end
188
+
175
189
  def join_headers(stream, request)
176
- set_request_headers(request)
177
- headers = {}
178
- headers[":scheme"] = request.scheme
179
- headers[":method"] = request.verb.to_s.upcase
180
- headers[":path"] = headline_uri(request)
181
- headers[":authority"] = request.authority
182
- headers = headers.merge(request.headers)
183
190
  log(level: 1, color: :yellow) do
184
- headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
191
+ request.headers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
185
192
  end
186
- stream.headers(headers, end_stream: request.empty?)
193
+ stream.headers(request.headers.each, end_stream: request.empty?)
187
194
  end
188
195
 
189
196
  def join_body(stream, request)
@@ -227,10 +234,15 @@ module HTTPX
227
234
  end
228
235
 
229
236
  def on_stream_close(stream, request, error)
237
+ log(level: 2) { "#{stream.id}: closing stream" }
238
+ @drains.delete(request)
239
+ @streams.delete(request)
240
+
230
241
  if error && error != :no_error
231
242
  ex = Error.new(stream.id, error)
232
243
  ex.set_backtrace(caller)
233
- emit(:error, request, ex)
244
+ response = ErrorResponse.new(request, ex, request.options)
245
+ emit(:response, request, response)
234
246
  else
235
247
  response = request.response
236
248
  if response.status == 421
@@ -241,9 +253,6 @@ module HTTPX
241
253
  emit(:response, request, response)
242
254
  end
243
255
  end
244
- log(level: 2) { "#{stream.id}: closing stream" }
245
-
246
- @streams.delete(request)
247
256
  send(@pending.shift) unless @pending.empty?
248
257
  return unless @streams.empty? && exhausted?
249
258
 
@@ -328,11 +337,9 @@ module HTTPX
328
337
  end
329
338
 
330
339
  def method_missing(meth, *args, &blk)
331
- if @connection.respond_to?(meth)
332
- @connection.__send__(meth, *args, &blk)
333
- else
334
- super
335
- end
340
+ return super unless @connection.respond_to?(meth)
341
+
342
+ @connection.__send__(meth, *args, &blk)
336
343
  end
337
344
  end
338
345
  Connection.register "h2", Connection::HTTP2