httpx 0.13.0 → 0.14.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_10_1.md +1 -1
  3. data/doc/release_notes/0_13_0.md +2 -2
  4. data/doc/release_notes/0_13_1.md +5 -0
  5. data/doc/release_notes/0_13_2.md +9 -0
  6. data/doc/release_notes/0_14_0.md +79 -0
  7. data/doc/release_notes/0_14_1.md +7 -0
  8. data/doc/release_notes/0_14_2.md +6 -0
  9. data/lib/httpx.rb +1 -2
  10. data/lib/httpx/callbacks.rb +12 -3
  11. data/lib/httpx/connection.rb +12 -9
  12. data/lib/httpx/connection/http1.rb +32 -14
  13. data/lib/httpx/connection/http2.rb +61 -15
  14. data/lib/httpx/headers.rb +7 -3
  15. data/lib/httpx/io/tcp.rb +3 -1
  16. data/lib/httpx/io/udp.rb +31 -7
  17. data/lib/httpx/options.rb +91 -56
  18. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  19. data/lib/httpx/plugins/aws_sigv4.rb +5 -4
  20. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  21. data/lib/httpx/plugins/compression.rb +8 -8
  22. data/lib/httpx/plugins/compression/brotli.rb +4 -3
  23. data/lib/httpx/plugins/compression/deflate.rb +4 -3
  24. data/lib/httpx/plugins/compression/gzip.rb +2 -1
  25. data/lib/httpx/plugins/cookies.rb +3 -7
  26. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  27. data/lib/httpx/plugins/expect.rb +6 -6
  28. data/lib/httpx/plugins/follow_redirects.rb +3 -3
  29. data/lib/httpx/plugins/grpc.rb +247 -0
  30. data/lib/httpx/plugins/grpc/call.rb +62 -0
  31. data/lib/httpx/plugins/grpc/message.rb +85 -0
  32. data/lib/httpx/plugins/multipart/part.rb +2 -2
  33. data/lib/httpx/plugins/proxy.rb +3 -7
  34. data/lib/httpx/plugins/proxy/http.rb +5 -4
  35. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  36. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  37. data/lib/httpx/plugins/retries.rb +13 -14
  38. data/lib/httpx/plugins/stream.rb +96 -74
  39. data/lib/httpx/plugins/upgrade.rb +6 -5
  40. data/lib/httpx/request.rb +25 -2
  41. data/lib/httpx/resolver/native.rb +7 -3
  42. data/lib/httpx/response.rb +4 -0
  43. data/lib/httpx/session.rb +17 -7
  44. data/lib/httpx/transcoder/chunker.rb +1 -1
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/callbacks.rbs +2 -0
  47. data/sig/connection/http1.rbs +5 -1
  48. data/sig/connection/http2.rbs +6 -2
  49. data/sig/headers.rbs +2 -2
  50. data/sig/options.rbs +9 -2
  51. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  52. data/sig/plugins/basic_authentication.rbs +2 -0
  53. data/sig/plugins/compression.rbs +2 -2
  54. data/sig/plugins/multipart.rbs +1 -1
  55. data/sig/plugins/stream.rbs +17 -16
  56. data/sig/request.rbs +7 -2
  57. data/sig/response.rbs +1 -0
  58. data/sig/session.rbs +4 -0
  59. metadata +18 -7
  60. data/lib/httpx/timeout.rb +0 -67
  61. data/sig/timeout.rbs +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecaac1dac4ce953ba1585a418432d9bf635aea581a6d81acb32778ff947f486b
4
- data.tar.gz: 32d9c5f65bd18ceeab524c6734081740ea2a3cbe526d3a6a71e079846f047644
3
+ metadata.gz: 5049a0cfb698c15800ea618a2775831183765f24ccb946c2adfb6b067c976c4d
4
+ data.tar.gz: 498a626981251e0ffbff3dde9d63fcc57e0b3183d29fafbc1ecd921d3bfe8172
5
5
  SHA512:
6
- metadata.gz: 204b0aea0e3bc09eccd31c9b93c79de866cff737801bca75096a61ea3ff45e62d44b2e9d39e26e3fb34d261dc518e17766080549f55b1cf0e16cfcd55565ddb4
7
- data.tar.gz: ffc06e694b6afd9e29d48857c0251b75829d41637583f45243dd7b30931f0b9a4f8bf06d13421e94dc5f07bf5113ff0f9f2c888be6313c9506e6da05659392a1
6
+ metadata.gz: 6fb23f5a45bd521bf30c25b8e30af3b35e4359093a470fb29f9ab6c8f5d0f09674a08b28ba2c20f92c51fe46702f4f3751b855b6b31f202be01728f56273d6a2
7
+ data.tar.gz: f64d1057e2483502dc3751a74489cfec0e7672d906f5901a81f98227c98ee907dc6be0b8d0ad7f4c4688546a2a7c22886f437807e28ba5b2a5e09c6b588de023
@@ -26,7 +26,7 @@ From now on, both headers and the responnse payload will also appear, so expecte
26
26
  ## Bugfixes
27
27
 
28
28
  * HTTP/2 and HTTP/1.1 exhausted connections now get properly migrated into a new connection;
29
- * HTTP/2 421 responses will now correctly migrate the connection and pendign requests to HTTP/1.1 (a hanging loop was being caused);
29
+ * HTTP/2 421 responses will now correctly migrate the connection and pending requests to HTTP/1.1 (a hanging loop was being caused);
30
30
  * HTTP/2 connection failed with a GOAWAY settings timeout will now return error responses (instead of hanging indefinitely);
31
31
  * Non-IP proxy name-resolving errors will now move on to the next available proxy in the list (instead of hanging indefinitely);
32
32
  * Non-IP DNS resolve errors for `native` and `https` variants will now return the appropriate error response (instead of hanging indefinitely);
@@ -41,7 +41,7 @@ The `:transport_options` are therefore deprecated, and will be moved in a major
41
41
 
42
42
  ## Improvements
43
43
 
44
- Some internal improvements that allow certainn plugins not to "leak" globally, such as the `:compression` plugin, which used to enable compression for all the `httpx` sessions from the same process. It doesn't anymore.
44
+ Some internal improvements that allow certain plugins not to "leak" globally, such as the `:compression` plugin, which used to enable compression for all the `httpx` sessions from the same process. It doesn't anymore.
45
45
 
46
46
  Using exceptionless nonblocking connect calls in the supported rubies.
47
47
 
@@ -55,4 +55,4 @@ When passing open IO objects for origins (the `:io` option), `httpx` was still t
55
55
 
56
56
  Fixed usage of `:io` option when passed an "authority/io" hash.
57
57
 
58
- Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
58
+ Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
@@ -0,0 +1,5 @@
1
+ # 0.13.1
2
+
3
+ ## Bugfixes
4
+
5
+ Rescue `Errno::EALREADY` on calls to `connect_nonblock(exception: false)` (there are exceptions after all...).
@@ -0,0 +1,9 @@
1
+ # 0.13.1
2
+
3
+ ## Improvements
4
+
5
+ `UDPSocket#sendmsg_nonblock` is now used in the native resolver.
6
+
7
+ ## Bugfixes
8
+
9
+ Usage in Windows was buggy, resulting in `Errno::EINVAL` during DNS resolving, when using the native resolver. This was due to a discrepancy between `recvfrom` behaviour in WS Sockets and Linux Sockets. This was fixed by making we the UDP socket never tries to receive before a DNS query has been actually sent.
@@ -0,0 +1,79 @@
1
+ # 0.14.0
2
+
3
+ ## Features
4
+
5
+ ### GRPC plugin
6
+
7
+ A new plugin, `:grpc`, is now available. This plugin provides a simple DSL to build GRPC services and performing calls using `httpx` under the hood.
8
+
9
+ Example:
10
+
11
+ ```ruby
12
+ require "httpx"
13
+
14
+ grpc = HTTPX.plugin(:grpc)
15
+ helloworld_stub = grpc.build_stub("localhost:4545")
16
+ helloworld_svc = helloworld_stub.rpc(:SayHello, HelloRequest, HelloReply)
17
+ result = helloworld_svc.say_hello(HelloRequest.new(name: "Jack")) #=> HelloReply: "Hello Jack"
18
+ ```
19
+
20
+ You can read more about the `:grpc` plugin in the [wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/GRPC).
21
+
22
+ ### :origin
23
+
24
+ A new `:origin` option is available. You can use it for setting a base URL for subsequent relative paths on that session:
25
+
26
+ ```ruby
27
+ HTTPX.get("/httpbin/get") #=> HTTPX::Error: invalid URI: /httpbin/get
28
+
29
+ httpbin = HTTPX.with(origin: "https://nghttp2.org")
30
+ httpbin.get("/httpbin/get") #=> #<Response:5420 HTTP/2.0 @status=200 ....
31
+ ```
32
+
33
+ **Note!** The origin is **not** for setting base paths, i.e. if you pass it a relative path, it'll be filtered out in subsequent requests (`HTTPX.with(origin: "https://nghttp2.org/httpbin")` will still use only `"https://nghttp2.org"`).
34
+
35
+ ## Improvements
36
+
37
+ * setting an unexpected option will now raise an `HTTPX::Error` with an helpful message, instead of a confusing `NoMethodError`:
38
+
39
+ ```ruby
40
+ HTTPX.with(foo: "bar")
41
+ # before
42
+ #=> NoMethodError
43
+ # after
44
+ #=> HTTPX::Error: unknown option: foo
45
+
46
+ ```
47
+
48
+ * `HTTPX::Options#def_option` (which can be used for setting custom plugin options) can now be passed a full body string (where the argument is `value`), although it still support the block form. This is the recommended approach, as the block form is based on `define_method`, which would make clients unusable inside ractors.
49
+
50
+ * Added support for `:wait_for_handshake` under the `http2_settings` option (`false` by default). HTTP/2 connections complete the protocol handshake before requests are sent. When this option is `true`, requests get send in the initial payload, before the HTTP/2 connection is fully acknowledged.
51
+
52
+ * 441716a5ac0f7707211ebe0048f568cf0b759c3f: The `:stream` plugin has been improved to start streaming the real response as methods are called (instead of a completely separate synchronous one, which is definitely not good):
53
+
54
+ ```ruby
55
+ session = HTTPX.plugin(:stream)
56
+ response = session.get(build_uri("/stream/3"), stream: true)
57
+
58
+ # before
59
+ response.status # this could block indefinitely, if the request truly streams infinitely.
60
+
61
+ # after
62
+ response.status # sends the request, and starts streaming the response until status is available.
63
+ response.each {|chunk|...} # and now you can start yielding the chunks...
64
+ ```
65
+
66
+
67
+ ## Bugfixes
68
+
69
+ * fixed usage of the `:multipart` if `pathname` isn't loaded.
70
+ * fixed HTTP/2 trailers.
71
+ * fixed connection merges with the same origin, which was causing them to be duplicated and breaking further usage. (#125)
72
+ * fixed repeated session callbacks on a connection, by ensure they're set only once.
73
+ * fixed calculation of `content-length` for streaming or chunked compressed requests.
74
+
75
+
76
+ ## Chore
77
+
78
+ * using ruby base container images in CI instead.
79
+ * using truffleruby official container image.
@@ -0,0 +1,7 @@
1
+ # 0.14.1
2
+
3
+
4
+ ## Bugfixes
5
+
6
+ * fixed: HTTP/2-specific headers were being reused on insecure redirects, thereby creating an invalid request (#128);
7
+ * fixed: multipart request parts weren't using explicity set `:content_type`, instead using file mime type or "text/plain";
@@ -0,0 +1,6 @@
1
+ # 0.14.2
2
+
3
+
4
+ ## Bugfixes
5
+
6
+ * fixed: multipart request parts weren't using explicity set `:filename`.
data/lib/httpx.rb CHANGED
@@ -12,12 +12,11 @@ require "httpx/callbacks"
12
12
  require "httpx/loggable"
13
13
  require "httpx/registry"
14
14
  require "httpx/transcoder"
15
- require "httpx/options"
16
- require "httpx/timeout"
17
15
  require "httpx/pool"
18
16
  require "httpx/headers"
19
17
  require "httpx/request"
20
18
  require "httpx/response"
19
+ require "httpx/options"
21
20
  require "httpx/chainable"
22
21
 
23
22
  # Top-Level Namespace
@@ -6,19 +6,28 @@ module HTTPX
6
6
  callbacks(type) << action
7
7
  end
8
8
 
9
- def once(event, &block)
10
- on(event) do |*args, &callback|
9
+ def once(type, &block)
10
+ on(type) do |*args, &callback|
11
11
  block.call(*args, &callback)
12
12
  :delete
13
13
  end
14
14
  end
15
15
 
16
+ def only(type, &block)
17
+ callbacks(type).clear
18
+ on(type, &block)
19
+ end
20
+
16
21
  def emit(type, *args)
17
- callbacks(type).delete_if { |pr| pr[*args] == :delete }
22
+ callbacks(type).delete_if { |pr| :delete == pr[*args] } # rubocop:disable Style/YodaCondition
18
23
  end
19
24
 
20
25
  protected
21
26
 
27
+ def callbacks_for?(type)
28
+ @callbacks.key?(type) && !@callbacks[type].empty?
29
+ end
30
+
22
31
  def callbacks(type = nil)
23
32
  return @callbacks unless type
24
33
 
@@ -69,7 +69,7 @@ module HTTPX
69
69
  end
70
70
 
71
71
  @inflight = 0
72
- @keep_alive_timeout = options.timeout.keep_alive_timeout
72
+ @keep_alive_timeout = options.timeout[:keep_alive_timeout]
73
73
  @keep_alive_timer = nil
74
74
 
75
75
  self.addresses = options.addresses if options.addresses
@@ -129,7 +129,7 @@ module HTTPX
129
129
  end
130
130
 
131
131
  def merge(connection)
132
- @origins += connection.instance_variable_get(:@origins)
132
+ @origins |= connection.instance_variable_get(:@origins)
133
133
  connection.purge_pending do |req|
134
134
  send(req)
135
135
  end
@@ -238,9 +238,9 @@ module HTTPX
238
238
  def timeout
239
239
  return @timeout if defined?(@timeout)
240
240
 
241
- return @options.timeout.connect_timeout if @state == :idle
241
+ return @options.timeout[:connect_timeout] if @state == :idle
242
242
 
243
- @options.timeout.operation_timeout
243
+ @options.timeout[:operation_timeout]
244
244
  end
245
245
 
246
246
  private
@@ -413,7 +413,7 @@ module HTTPX
413
413
  emit(:exhausted)
414
414
  end
415
415
  parser.on(:origin) do |origin|
416
- @origins << origin
416
+ @origins |= [origin]
417
417
  end
418
418
  parser.on(:close) do |force|
419
419
  transition(:closing)
@@ -443,6 +443,7 @@ module HTTPX
443
443
  emit(:misdirected, request)
444
444
  else
445
445
  response = ErrorResponse.new(request, ex, @options)
446
+ request.response = response
446
447
  request.emit(:response, response)
447
448
  end
448
449
  end
@@ -451,7 +452,7 @@ module HTTPX
451
452
  def transition(nextstate)
452
453
  case nextstate
453
454
  when :idle
454
- @timeout = @current_timeout = @options.timeout.connect_timeout
455
+ @timeout = @current_timeout = @options.timeout[:connect_timeout]
455
456
 
456
457
  when :open
457
458
  return if @state == :closed
@@ -463,7 +464,7 @@ module HTTPX
463
464
 
464
465
  send_pending
465
466
 
466
- @timeout = @current_timeout = @options.timeout.operation_timeout
467
+ @timeout = @current_timeout = @options.timeout[:operation_timeout]
467
468
  emit(:open)
468
469
  when :closing
469
470
  return unless @state == :open
@@ -542,12 +543,14 @@ module HTTPX
542
543
  def handle_error(error)
543
544
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
544
545
  while (request = @pending.shift)
545
- request.emit(:response, ErrorResponse.new(request, error, @options))
546
+ response = ErrorResponse.new(request, error, @options)
547
+ request.response = response
548
+ request.emit(:response, response)
546
549
  end
547
550
  end
548
551
 
549
552
  def total_timeout
550
- total = @options.timeout.total_timeout
553
+ total = @options.timeout[:total_timeout]
551
554
 
552
555
  return unless total
553
556
 
@@ -254,12 +254,15 @@ module HTTPX
254
254
  end
255
255
 
256
256
  def set_protocol_headers(request)
257
- request.headers["host"] ||= request.authority
258
- request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
259
257
  if !request.headers.key?("content-length") &&
260
258
  request.body.bytesize == Float::INFINITY
261
259
  request.chunk!
262
260
  end
261
+
262
+ {
263
+ "host" => (request.headers["host"] || request.authority),
264
+ "connection" => (request.headers["connection"] || (request.options.persistent ? "keep-alive" : "close")),
265
+ }
263
266
  end
264
267
 
265
268
  def headline_uri(request)
@@ -272,23 +275,18 @@ module HTTPX
272
275
  join_headers(request) if request.state == :headers
273
276
  request.transition(:body)
274
277
  join_body(request) if request.state == :body
278
+ request.transition(:trailers)
279
+ # HTTP/1.1 trailers should only work for chunked encoding
280
+ join_trailers(request) if request.body.chunked? && request.state == :trailers
275
281
  request.transition(:done)
276
282
  end
277
283
  end
278
284
 
279
285
  def join_headers(request)
280
- buffer = +""
281
- buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
282
- log(color: :yellow) { "<- HEADLINE: #{buffer.chomp.inspect}" }
283
- @buffer << buffer
284
- buffer.clear
285
- set_protocol_headers(request)
286
- request.headers.each do |field, value|
287
- buffer << "#{capitalized(field)}: #{value}" << CRLF
288
- log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
289
- @buffer << buffer
290
- buffer.clear
291
- end
286
+ @buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
287
+ log(color: :yellow) { "<- HEADLINE: #{@buffer.to_s.chomp.inspect}" }
288
+ extra_headers = set_protocol_headers(request)
289
+ join_headers2(request.headers.each(extra_headers))
292
290
  log { "<- " }
293
291
  @buffer << CRLF
294
292
  end
@@ -302,6 +300,26 @@ module HTTPX
302
300
  @buffer << chunk
303
301
  throw(:buffer_full, request) if @buffer.full?
304
302
  end
303
+
304
+ raise request.drain_error if request.drain_error
305
+ end
306
+
307
+ def join_trailers(request)
308
+ return unless request.trailers? && request.callbacks_for?(:trailers)
309
+
310
+ join_headers2(request.trailers)
311
+ log { "<- " }
312
+ @buffer << CRLF
313
+ end
314
+
315
+ def join_headers2(headers)
316
+ buffer = "".b
317
+ headers.each do |field, value|
318
+ buffer << "#{capitalized(field)}: #{value}" << CRLF
319
+ log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
320
+ @buffer << buffer
321
+ buffer.clear
322
+ end
305
323
  end
306
324
 
307
325
  UPCASED = {
@@ -21,14 +21,16 @@ module HTTPX
21
21
 
22
22
  def initialize(buffer, options)
23
23
  @options = Options.new(options)
24
- @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
25
- @max_requests = @options.max_requests || 0
24
+ @settings = @options.http2_settings
26
25
  @pending = []
27
26
  @streams = {}
28
27
  @drains = {}
29
28
  @pings = []
30
29
  @buffer = buffer
31
30
  @handshake_completed = false
31
+ @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
32
+ @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
33
+ @max_requests = @options.max_requests || 0
32
34
  init_connection
33
35
  end
34
36
 
@@ -36,7 +38,11 @@ module HTTPX
36
38
  # waiting for WINDOW_UPDATE frames
37
39
  return :r if @buffer.full?
38
40
 
39
- return :w if @connection.state == :closed
41
+ if @connection.state == :closed
42
+ return unless @handshake_completed
43
+
44
+ return :w
45
+ end
40
46
 
41
47
  unless (@connection.state == :connected && @handshake_completed)
42
48
  return @buffer.empty? ? :r : :rw
@@ -75,9 +81,13 @@ module HTTPX
75
81
  end
76
82
 
77
83
  def can_buffer_more_requests?
78
- @handshake_completed &&
84
+ if @handshake_completed
79
85
  @streams.size < @max_concurrent_requests &&
80
- @streams.size < @max_requests
86
+ @streams.size < @max_requests
87
+ else
88
+ !@wait_for_handshake &&
89
+ @streams.size < @max_concurrent_requests
90
+ end
81
91
  end
82
92
 
83
93
  def send(request)
@@ -140,12 +150,14 @@ module HTTPX
140
150
  join_headers(stream, request) if request.state == :headers
141
151
  request.transition(:body)
142
152
  join_body(stream, request) if request.state == :body
153
+ request.transition(:trailers)
154
+ join_trailers(stream, request) if request.state == :trailers && !request.body.empty?
143
155
  request.transition(:done)
144
156
  end
145
157
  end
146
158
 
147
159
  def init_connection
148
- @connection = HTTP2Next::Client.new(@options.http2_settings)
160
+ @connection = HTTP2Next::Client.new(@settings)
149
161
  @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
150
162
  @connection.on(:frame, &method(:on_frame))
151
163
  @connection.on(:frame_sent, &method(:on_frame_sent))
@@ -169,6 +181,7 @@ module HTTPX
169
181
  public :reset
170
182
 
171
183
  def handle_stream(stream, request)
184
+ request.on(:refuse, &method(:on_stream_refuse).curry(3)[stream, request])
172
185
  stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
173
186
  stream.on(:half_close) do
174
187
  log(level: 2) { "#{stream.id}: waiting for response..." }
@@ -179,18 +192,32 @@ module HTTPX
179
192
  end
180
193
 
181
194
  def set_protocol_headers(request)
182
- request.headers[":scheme"] = request.scheme
183
- request.headers[":method"] = request.verb.to_s.upcase
184
- request.headers[":path"] = headline_uri(request)
185
- request.headers[":authority"] = request.authority
195
+ {
196
+ ":scheme" => request.scheme,
197
+ ":method" => request.verb.to_s.upcase,
198
+ ":path" => headline_uri(request),
199
+ ":authority" => request.authority,
200
+ }
186
201
  end
187
202
 
188
203
  def join_headers(stream, request)
189
- set_protocol_headers(request)
204
+ extra_headers = set_protocol_headers(request)
205
+ log(level: 1, color: :yellow) do
206
+ request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
207
+ end
208
+ stream.headers(request.headers.each(extra_headers), end_stream: request.empty?)
209
+ end
210
+
211
+ def join_trailers(stream, request)
212
+ unless request.trailers?
213
+ stream.data("", end_stream: true) if request.callbacks_for?(:trailers)
214
+ return
215
+ end
216
+
190
217
  log(level: 1, color: :yellow) do
191
- request.headers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
218
+ request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
192
219
  end
193
- stream.headers(request.headers.each, end_stream: request.empty?)
220
+ stream.headers(request.trailers.each, end_stream: true)
194
221
  end
195
222
 
196
223
  def join_body(stream, request)
@@ -201,13 +228,15 @@ module HTTPX
201
228
  next_chunk = request.drain_body
202
229
  log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
203
230
  log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
204
- stream.data(chunk, end_stream: !next_chunk)
205
- if next_chunk && @buffer.full?
231
+ stream.data(chunk, end_stream: !(next_chunk || request.trailers? || request.callbacks_for?(:trailers)))
232
+ if next_chunk && (@buffer.full? || request.body.unbounded_body?)
206
233
  @drains[request] = next_chunk
207
234
  throw(:buffer_full)
208
235
  end
209
236
  chunk = next_chunk
210
237
  end
238
+
239
+ on_stream_refuse(stream, request, request.drain_error) if request.drain_error
211
240
  end
212
241
 
213
242
  ######
@@ -215,6 +244,11 @@ module HTTPX
215
244
  ######
216
245
 
217
246
  def on_stream_headers(stream, request, h)
247
+ if request.response && request.response.version == "2.0"
248
+ on_stream_trailers(stream, request, h)
249
+ return
250
+ end
251
+
218
252
  log(color: :yellow) do
219
253
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
220
254
  end
@@ -227,12 +261,24 @@ module HTTPX
227
261
  handle(request, stream) if request.expects?
228
262
  end
229
263
 
264
+ def on_stream_trailers(stream, request, h)
265
+ log(color: :yellow) do
266
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
267
+ end
268
+ request.response.merge_headers(h)
269
+ end
270
+
230
271
  def on_stream_data(stream, request, data)
231
272
  log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
232
273
  log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
233
274
  request.response << data
234
275
  end
235
276
 
277
+ def on_stream_refuse(stream, request, error)
278
+ stream.close
279
+ on_stream_close(stream, request, error)
280
+ end
281
+
236
282
  def on_stream_close(stream, request, error)
237
283
  log(level: 2) { "#{stream.id}: closing stream" }
238
284
  @drains.delete(request)