httpx 1.7.8 → 1.8.1

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +1 -2
  3. data/doc/release_notes/1_8_0.md +100 -0
  4. data/doc/release_notes/1_8_1.md +27 -0
  5. data/lib/httpx/adapters/datadog.rb +12 -1
  6. data/lib/httpx/adapters/faraday.rb +2 -2
  7. data/lib/httpx/adapters/webmock.rb +6 -0
  8. data/lib/httpx/connection/http1.rb +15 -12
  9. data/lib/httpx/connection/http2.rb +69 -9
  10. data/lib/httpx/connection.rb +119 -21
  11. data/lib/httpx/errors.rb +8 -1
  12. data/lib/httpx/headers.rb +2 -2
  13. data/lib/httpx/io/ssl.rb +8 -9
  14. data/lib/httpx/io/tcp.rb +22 -5
  15. data/lib/httpx/io/unix.rb +2 -2
  16. data/lib/httpx/loggable.rb +11 -4
  17. data/lib/httpx/options.rb +16 -4
  18. data/lib/httpx/parser/http1.rb +8 -2
  19. data/lib/httpx/plugins/auth/digest.rb +1 -1
  20. data/lib/httpx/plugins/auth.rb +61 -4
  21. data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
  22. data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
  23. data/lib/httpx/plugins/cache.rb +221 -0
  24. data/lib/httpx/plugins/callbacks.rb +12 -0
  25. data/lib/httpx/plugins/digest_auth.rb +4 -0
  26. data/lib/httpx/plugins/fiber_concurrency.rb +75 -4
  27. data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
  28. data/lib/httpx/plugins/oauth.rb +66 -14
  29. data/lib/httpx/plugins/persistent.rb +18 -2
  30. data/lib/httpx/plugins/proxy.rb +5 -0
  31. data/lib/httpx/plugins/push_promise.rb +2 -2
  32. data/lib/httpx/plugins/response_cache.rb +26 -105
  33. data/lib/httpx/plugins/retries.rb +7 -5
  34. data/lib/httpx/plugins/server_sent_events.rb +158 -0
  35. data/lib/httpx/plugins/ssrf_filter.rb +17 -2
  36. data/lib/httpx/plugins/stream.rb +9 -5
  37. data/lib/httpx/plugins/stream_bidi.rb +2 -0
  38. data/lib/httpx/plugins/tracing.rb +15 -4
  39. data/lib/httpx/pool.rb +2 -2
  40. data/lib/httpx/request/body.rb +2 -2
  41. data/lib/httpx/request.rb +42 -10
  42. data/lib/httpx/resolver/cache/base.rb +1 -8
  43. data/lib/httpx/resolver/cache/file.rb +56 -0
  44. data/lib/httpx/resolver/https.rb +20 -20
  45. data/lib/httpx/resolver/native.rb +21 -3
  46. data/lib/httpx/resolver/resolver.rb +4 -0
  47. data/lib/httpx/resolver/system.rb +5 -2
  48. data/lib/httpx/response/body.rb +6 -4
  49. data/lib/httpx/response.rb +12 -5
  50. data/lib/httpx/selector.rb +12 -1
  51. data/lib/httpx/session.rb +6 -1
  52. data/lib/httpx/session_extensions.rb +2 -2
  53. data/lib/httpx/timers.rb +3 -0
  54. data/lib/httpx/version.rb +1 -1
  55. data/sig/chainable.rbs +3 -0
  56. data/sig/connection/http1.rbs +1 -1
  57. data/sig/connection/http2.rbs +4 -1
  58. data/sig/connection.rbs +16 -9
  59. data/sig/errors.rbs +9 -3
  60. data/sig/httpx.rbs +2 -0
  61. data/sig/io/tcp.rbs +2 -0
  62. data/sig/loggable.rbs +8 -0
  63. data/sig/options.rbs +25 -12
  64. data/sig/parser/http1.rbs +3 -1
  65. data/sig/plugins/auth/ntlm.rbs +1 -1
  66. data/sig/plugins/auth.rbs +4 -0
  67. data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
  68. data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
  69. data/sig/plugins/cache.rbs +69 -0
  70. data/sig/plugins/fiber_concurrency.rbs +4 -0
  71. data/sig/plugins/ntlm_v2_auth.rbs +36 -0
  72. data/sig/plugins/persistent.rbs +3 -0
  73. data/sig/plugins/response_cache.rbs +13 -38
  74. data/sig/plugins/retries.rbs +5 -5
  75. data/sig/plugins/server_sent_events.rbs +45 -0
  76. data/sig/plugins/ssrf_filter.rbs +5 -1
  77. data/sig/plugins/stream.rbs +1 -1
  78. data/sig/plugins/stream_bidi.rbs +0 -2
  79. data/sig/plugins/webdav.rbs +1 -1
  80. data/sig/pool.rbs +2 -2
  81. data/sig/request.rbs +9 -6
  82. data/sig/resolver/cache/file.rbs +13 -0
  83. data/sig/resolver/entry.rbs +1 -1
  84. data/sig/resolver/https.rbs +3 -3
  85. data/sig/resolver/multi.rbs +1 -1
  86. data/sig/resolver/native.rbs +5 -5
  87. data/sig/resolver/resolver.rbs +1 -3
  88. data/sig/resolver/system.rbs +5 -2
  89. data/sig/resolver.rbs +3 -0
  90. data/sig/response.rbs +3 -0
  91. data/sig/selector.rbs +13 -8
  92. data/sig/timers.rbs +7 -5
  93. data/sig/transcoder/body.rbs +1 -1
  94. data/sig/transcoder/gzip.rbs +3 -2
  95. data/sig/transcoder/multipart.rbs +4 -1
  96. data/sig/transcoder/utils/deflater.rbs +2 -0
  97. data/sig/transcoder.rbs +2 -0
  98. data/sig/utils.rbs +1 -1
  99. metadata +19 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b725c719f0e33c27285f1f228dc9c9b19869d65f43d75c9e7b7f95dc8f262278
4
- data.tar.gz: 92efa77ade2bae77b3d983a08038305a557953653f5e37236254b12060b0a494
3
+ metadata.gz: eece37bd091a7fea17ccc62eca63acaee714c08658d081a938fc38495a4c8813
4
+ data.tar.gz: 72dcd038803106f08a0317c8d7bc97d22d6cdf5c469ba8f8f9d380d00e8f5ea7
5
5
  SHA512:
6
- metadata.gz: 2c52b06329f4d13d95242ec6043cf615e077a6dcdef6ad3cfe06d8893eb27555df4d5cdbcd95b621f9d43b4c40281becfbdb4005a074ea34463ca6e951537363
7
- data.tar.gz: 4060403c67050b2d9603ec801330d0e32d24c86bf10094ce396e646ab2649870981df0eb66f042a5da1835ba349ef69024148458b6fa3e23657ae2c1a8bc7cfc
6
+ metadata.gz: 5af43571f7da30f709883474fb61ef3661b31fa836d249c423726f4815fb7b2bf5c1281e22ca7ad15c00ed26b2ffc950d521b14a9f5c02e7d95d4198b4b986d0
7
+ data.tar.gz: bd73537be9a610b590d7897cd96f7b6213ece24ea7079ce995a3e05cbee363dcf0f575405ea8d2e612a64737e3bbc70dbdc25ab9a24f5b7e154b9a5187974ffe
@@ -17,5 +17,4 @@
17
17
  * `:stream_bidi` plugin: fix internal state preventing bidi requests from being retried.
18
18
  * `:stream_bidi` plugin: will only allow initial request body being passed using `:body` param (others, like `:json`, will raise an exception)
19
19
  * https resolver: return `:idle` on `#state` calls when no connection is available (sometimes called in internal log messages).
20
- * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
21
- *
20
+ * selector loop fix: when there are no selectables and an interval is passed, sleep instead of returning (thereby avoiding potential busy loop).
@@ -0,0 +1,100 @@
1
+ # 1.8.0
2
+
3
+ ## Features
4
+
5
+ ### New plugins
6
+
7
+ #### `:server_sent_events` plugin
8
+
9
+ The `:server_sent_events` plugin provides a convenience API to deal with `text/event-stream` requests, on top of the `:stream` plugin.
10
+
11
+ ```ruby
12
+ session = HTTPX.plugin(:server_sent_events)
13
+
14
+ sse_response = session.get("https://example.com/event-stream", event_stream: true)
15
+
16
+ sse_response.each_message do |message|
17
+ puts message.id
18
+ puts message.event
19
+ puts message.data
20
+ end
21
+ ```
22
+
23
+ You can read more about it in https://gitlab.com/os85/httpx/wikis/Server-Sent-Events .
24
+
25
+ #### `:cache` plugin
26
+
27
+ The `:cache` plugin allows caching responses. It exposes some options to determine some of its functionality, i.e. whether a request can use a cached response, whether a response can be cached, whether a cached response is still valid, etc.
28
+
29
+ This functionality was extracted from the `:response_cache` plugin, which now uses it under the hood.
30
+
31
+ You can read more about it in https://gitlab.com/os85/httpx/wikis/Cache .
32
+
33
+ #### `:ntlm_v2_auth` plugin
34
+
35
+ The `:ntlm_v2_auth` plugin is now available. It implements the most recent version of the NTLM authentication scheme supported by Microsoft products.
36
+
37
+ You can read more about it in https://gitlab.com/os85/httpx/wikis/Auth#ntlm-v2-auth .
38
+
39
+ ### New timeouts
40
+
41
+ #### `:total_request_timeout`
42
+
43
+ You can use the `:total_request_timeout` to time the time it takes a request to get its final response. This includes when your requests follows redirects (via the `:follow_redirects` plugin) or is retried multiple times (via the `:retries´ plugin).
44
+
45
+ #### `:ping_timeout`
46
+
47
+ Defines the number of seconds a connection waits to receive a ping response when probing for a connection for liveness.
48
+
49
+ Defaults to 2 seconds.
50
+
51
+ ### `:ssrf_filter` plugin new options
52
+
53
+ #### `:extra_unsafe_ranges`
54
+
55
+ A list of extra unsafe IPs or IP ranges to the default deny list.
56
+
57
+ #### `:safe_private_ranges`
58
+
59
+ A list of IPs or IP ranges which are allowed and would otherwise be denied.
60
+
61
+ ### `:auth` plugin new option
62
+
63
+ #### `:reset_auth_header_expires_in/at`
64
+
65
+ The `:reset_auth_header_expires_in` and `:reset_auth_header_expires_at` options enable discarding an authorization token an X number of seconds after it has been generated, or at a particuar point in time, respectively. This is useful when the token is dynamically generated, so that you can preemptively renegotiate a new one.
66
+
67
+ The `:oauth` plugin makes use of these fields, alongside the `expires_in` claim from token responses, to refresh the token as soon as the it expires.
68
+
69
+ ### `:max_response_body_size`
70
+
71
+ Can be set to the maximum number of bytes a response may have, after which it'll return an `HTTPX::ErrorResponse`. The limit is enforced based on the `content-length` header and as bytes are received.
72
+
73
+ ### `:max_response_headers`
74
+
75
+ Can be set to the maximum number of headers a response may have.
76
+
77
+ ### `:max_response_header_value_size`
78
+
79
+ Can be set to the maximum number of bytes a header value may have. In cases where a header field may be spread across multiple entries (ex. `"cookie"`), the limit is enforced on the aggregate byte size.
80
+
81
+ ### resolver file cache
82
+
83
+ By specifying `:file` as the resolver `:cache` option, the DNS entries will be cached in a known location in your file system. This will allow sharing entries across processes within the same machine to reduce overall DNS traffic. You can use it via:
84
+
85
+ ```ruby
86
+ session = HTTPX.with(resolver_options: { cache: :file })
87
+ session.get("https://example.com")
88
+ ```
89
+
90
+ ## Improvements
91
+
92
+ * `http-2` minimum version is now 1.2.0, which brings performance benefits around frame parsing and other common operations.
93
+
94
+ ## Bugfixes
95
+
96
+ * several fixes to make `httpx` usable inside a fiber scheduler in ruby 4.
97
+ * `:proxy` plugin: unescape user/password from proxy options before reusing it (to avoid using percent-encoded values when p.ex. generating base64-encoding for basic auth).
98
+ * `:retries` plugin: fixed the polynomial and exponential backoff `:retry_after` strategies calculation.
99
+ * `:tracing` plugin: fixing span start time set up when request is sent to a closed connection, or when a long-lived connection will be probed for liveness.
100
+ * datadog adapter: fixed integration with the `datadog` gem v2.34 or higher.
@@ -0,0 +1,27 @@
1
+ # 1.8.1
2
+
3
+ ## Improvements
4
+
5
+ * the session will now eagerly write initial data to connections instead of waiting for I/O the first time (saves a syscall).
6
+ * removed all rescue blocks rescuing `StandardError` internally; this could interfere with usages of `httpx` inside `Timeout.timeout` blocks, which may emit an exception at any point, including in blocks where that exception could be rescued, leaving the session in a potentially irrecoverable state and looping.
7
+ * debug logs change the display of ids from 'tid:123, fid:123' to 'tid=123 fid:123'.
8
+ * and when using the `datadog` adapter, all related correlation ids (trace id, span id, etc) will also be added to the identifiers in the same format
9
+ * plugins built on top of the `:retries` plugin will skip the `retry_after` interval when certain conditions are met.
10
+ * `:persistent` plugin: request will be retried immediately, when the error happens after the keep alive interval expired.
11
+ * `:auth` plugin: request will be retried immediately if it failed due to a "401 Unauthorized Error".
12
+ * `:ssrf_filter` plugin: removed public/private references of exception message.
13
+
14
+ ## Bugfixes
15
+
16
+ * `:auth` plugin: handle edge case where `:auth_header_expires_at` returns no value.
17
+ * `:digest_auth` plugin: do not retry if there was an issue building building the authorization header after the probe.
18
+ * `webmock` adapter: prevent mocked requests from opening a connection to the peer server that will not be used.
19
+ * clear and cancel request timers on completion or reset, ensuring that they won't be retained way past the point where the request is gone.
20
+ * connection: cancel and discard ping timer before closing (removes timer reference)
21
+ * connection: fix logic where purging-while-resending requests to a coalesced connection was leaving references in the old connection.
22
+ * moved an `on_body_chunk` callback to the `:callbacks` plugin, where it's used and expected to be defined.
23
+ * `http2`: avoid closing the connection on internal errors which are supposed to be specific to a request, such as the max headers or max response body size checks.
24
+ * system resolver will use an internal exception when using `Timeout.timeout` on resolutions to avoid conflicts with the default timeout exception class.
25
+ * prevent edge case where `#alpn_protocol` was being called on a `TCPSocket`.
26
+ * fixed v1.8.0 regression causing connection to loop when an error happens while connecting.
27
+ * resolver cache: fixed cache eviction logic, where alias answers weren't being evicted, causing resolutions to stall after a certain number of resolutions.
@@ -15,8 +15,10 @@ module Datadog::Tracing
15
15
 
16
16
  TAG_BASE_SERVICE = if Gem::Version.new(DATADOG_VERSION::STRING) < Gem::Version.new("1.15.0")
17
17
  "_dd.base_service"
18
- else
18
+ elsif Gem::Version.new(DATADOG_VERSION::STRING) < Gem::Version.new("2.34.0")
19
19
  Datadog::Tracing::Contrib::Ext::Metadata::TAG_BASE_SERVICE
20
+ else
21
+ Datadog::Tracing::Metadata::Ext::TAG_BASE_SERVICE
20
22
  end
21
23
  TAG_PEER_HOSTNAME = Datadog::Tracing::Metadata::Ext::TAG_PEER_HOSTNAME
22
24
  TAG_PEER_SERVICE = Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE
@@ -183,6 +185,13 @@ module Datadog::Tracing
183
185
  end
184
186
  end
185
187
 
188
+ # patches httpx debug logs to include datadog correlation ids
189
+ module LoggablePatch
190
+ def log_identifiers
191
+ "#{super} #{Datadog::Tracing.log_correlation}"
192
+ end
193
+ end
194
+
186
195
  module Configuration
187
196
  # Default settings for httpx
188
197
  #
@@ -295,6 +304,8 @@ module Datadog::Tracing
295
304
 
296
305
  ::HTTPX.send(:remove_const, :Session)
297
306
  ::HTTPX.send(:const_set, :Session, datadog_session.class)
307
+
308
+ ::HTTPX::Loggable.singleton_class.prepend(LoggablePatch)
298
309
  end
299
310
  end
300
311
 
@@ -115,11 +115,11 @@ module Faraday
115
115
  ssl_options
116
116
  end
117
117
  else
118
- # :nocov:
118
+ # simplecov:disable
119
119
  def ssl_options_from_env(*)
120
120
  {}
121
121
  end
122
- # :nocov:
122
+ # simplecov:enable
123
123
  end
124
124
  end
125
125
 
@@ -155,6 +155,12 @@ module WebMock
155
155
  raise WebMock::NetConnectNotAllowedError, request_signature
156
156
  end
157
157
  end
158
+
159
+ private
160
+
161
+ def connect
162
+ super unless @mocked
163
+ end
158
164
  end
159
165
  end
160
166
 
@@ -14,6 +14,7 @@ module HTTPX
14
14
  "www-authenticate" => "WWW-Authenticate",
15
15
  "http2-settings" => "HTTP2-Settings",
16
16
  "content-md5" => "Content-MD5",
17
+ "last-event-id" => "Last-Event-ID",
17
18
  }.freeze
18
19
  attr_reader :pending, :requests
19
20
 
@@ -23,7 +24,7 @@ module HTTPX
23
24
  @options = options
24
25
  @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
25
26
  @max_requests = @options.max_requests
26
- @parser = Parser::HTTP1.new(self)
27
+ @parser = Parser::HTTP1.new(self, options.max_response_headers, options.max_response_header_value_size)
27
28
  @buffer = buffer
28
29
  @version = [1, 1]
29
30
  @pending = []
@@ -47,6 +48,10 @@ module HTTPX
47
48
  end
48
49
 
49
50
  def reset
51
+ if @ping_timer
52
+ @ping_timer.cancel
53
+ @ping_timer = nil
54
+ end
50
55
  @max_requests = @options.max_requests || MAX_REQUESTS
51
56
  @parser.reset!
52
57
  @handshake_completed = false
@@ -132,6 +137,10 @@ module HTTPX
132
137
  request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
133
138
  request.log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
134
139
 
140
+ if response.content_length && response.content_length > request.options.max_response_body_size
141
+ raise HTTPX::Error, "maximum response body size exceeded"
142
+ end
143
+
135
144
  request.response = response
136
145
  on_complete if response.finished?
137
146
  end
@@ -153,18 +162,12 @@ module HTTPX
153
162
 
154
163
  return unless request
155
164
 
156
- begin
157
- request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
158
- request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
165
+ request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
166
+ request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
159
167
 
160
- response = request.response
168
+ response = request.response
161
169
 
162
- response << chunk
163
- rescue StandardError => e
164
- error_response = ErrorResponse.new(request, e)
165
- request.response = error_response
166
- dispatch(request)
167
- end
170
+ response << chunk
168
171
  end
169
172
 
170
173
  def on_complete
@@ -407,7 +410,7 @@ module HTTPX
407
410
  end
408
411
 
409
412
  def capitalized(field)
410
- UPCASED[field] || field.split("-").map(&:capitalize).join("-")
413
+ UPCASED.fetch(field) { field.split("-").map(&:capitalize).join("-") }
411
414
  end
412
415
  end
413
416
  end
@@ -37,6 +37,7 @@ module HTTPX
37
37
  @streams = {}
38
38
  @drains = {}
39
39
  @pings = []
40
+ @streams_to_close_after_receive = []
40
41
  @buffer = buffer
41
42
  @handshake_completed = false
42
43
  @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
@@ -110,6 +111,12 @@ module HTTPX
110
111
 
111
112
  def <<(data)
112
113
  @connection << data
114
+
115
+ while (stream, request, error = @streams_to_close_after_receive.shift)
116
+ # these streams were marked for cancellation due to errors found while processing the
117
+ # data received by the peer.
118
+ emit_stream_error(stream, request, error)
119
+ end
113
120
  end
114
121
 
115
122
  def send(request, head = false)
@@ -118,7 +125,7 @@ module HTTPX
118
125
  return false
119
126
  end
120
127
  unless (stream = @streams[request])
121
- stream = @connection.new_stream
128
+ stream = @connection.new_stream(**request.http2_stream_options)
122
129
  handle_stream(stream, request)
123
130
  @streams[request] = stream
124
131
  @max_requests -= 1
@@ -128,6 +135,8 @@ module HTTPX
128
135
  rescue ::HTTP2::Error::StreamLimitExceeded
129
136
  @pending.unshift(request)
130
137
  false
138
+ rescue ::HTTP2::Error::Error, ArgumentError => e
139
+ emit(:error, request, e)
131
140
  end
132
141
 
133
142
  def consume
@@ -312,11 +321,27 @@ module HTTPX
312
321
  end
313
322
  _, status = h.shift
314
323
  headers = request.options.headers_class.new(h)
324
+
325
+ raise HTTPX::Error, "maximum number of response headers exceeded" if h.size > @options.max_response_headers
326
+
327
+ if (max_header_value_size = @options.max_response_header_value_size)
328
+ headers.each do |_, v| # rubocop:disable Style/HashEachMethods
329
+ raise HTTPX::Error, "maximum header value size exceeded" if v.size > max_header_value_size
330
+ end
331
+ end
332
+
315
333
  response = request.options.response_class.new(request, status, "2.0", headers)
334
+
335
+ if response.content_length && response.content_length > request.options.max_response_body_size
336
+ raise HTTPX::Error.new, "maximum response body size exceeded"
337
+ end
338
+
316
339
  request.response = response
317
340
  @streams[request] = stream
318
341
 
319
342
  handle(request, stream) if request.expects?
343
+ rescue HTTPX::Error => e
344
+ @streams_to_close_after_receive << [stream, request, e]
320
345
  end
321
346
 
322
347
  def on_stream_trailers(stream, request, response, h)
@@ -329,7 +354,18 @@ module HTTPX
329
354
  def on_stream_data(stream, request, data)
330
355
  request.log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
331
356
  request.log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
357
+
358
+ return unless request.response
359
+
332
360
  request.response << data
361
+ rescue HTTPX::Error => e
362
+ if stream.state == :closing
363
+ # there won't be any more chunks to process after this one.
364
+ emit_stream_error(stream, request, e)
365
+ else
366
+ # defer until the last chunk from the payload is processed.
367
+ @streams_to_close_after_receive << [stream, request, e]
368
+ end
333
369
  end
334
370
 
335
371
  def on_stream_refuse(stream, request, error)
@@ -365,11 +401,12 @@ module HTTPX
365
401
  emit(:response, request, response)
366
402
  end
367
403
  else
368
- response = request.response
369
- if response && response.is_a?(Response) && response.status == 421
370
- emit(:error, request, :http_1_1_required)
371
- else
372
- emit(:response, request, response)
404
+ if (response = request.response)
405
+ if response.is_a?(Response) && response.status == 421
406
+ emit(:error, request, :http_1_1_required)
407
+ else
408
+ emit(:response, request, response)
409
+ end
373
410
  end
374
411
  end
375
412
  send(@pending.shift) unless @pending.empty?
@@ -428,11 +465,28 @@ module HTTPX
428
465
  end
429
466
 
430
467
  def frame_with_extra_info(frame)
468
+ flags_bits = frame.fetch(:flags, 0)
431
469
  case frame[:type]
432
470
  when :data
433
- frame.merge(payload: frame[:payload].bytesize)
434
- when :headers, :ping
435
- frame.merge(payload: log_redact_headers(frame[:payload]))
471
+ flags = [] #: Array[Symbol]
472
+ flags << :end_stream if flags_bits.anybits?(0b0001)
473
+ flags << :padded if flags_bits.anybits?(0b1000)
474
+ frame.merge(payload: frame[:payload].bytesize, flags: flags)
475
+ when :push_promise, :headers
476
+ flags = [] #: Array[Symbol]
477
+ flags << :end_stream if flags_bits.anybits?(0b0001)
478
+ flags << :priority if flags_bits.anybits?(0b0010)
479
+ flags << :end_headers if flags_bits.anybits?(0b0100)
480
+ flags << :padded if flags_bits.anybits?(0b1000)
481
+ frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
482
+ when :ping
483
+ flags = [] #: Array[Symbol]
484
+ flags << :ack if flags_bits.anybits?(0b0001)
485
+ frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
486
+ when :settings
487
+ flags = [] #: Array[Symbol]
488
+ flags << :ack if flags_bits.anybits?(0b0001)
489
+ frame.merge(flags: flags)
436
490
  when :window_update
437
491
  connection_or_stream = if (id = frame[:stream]).zero?
438
492
  @connection
@@ -476,6 +530,12 @@ module HTTPX
476
530
  emit(:pong)
477
531
  end
478
532
 
533
+ def emit_stream_error(stream, request, error)
534
+ teardown(request)
535
+ stream.close
536
+ emit(:error, request, error)
537
+ end
538
+
479
539
  def teardown(request = nil)
480
540
  if request
481
541
  @drains.delete(request)