httpx 0.13.0 → 0.14.2

Sign up to get free protection for your applications and to get access to all the features.
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)