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
@@ -47,8 +47,8 @@ module HTTPX
47
47
  def initialize(uri, options)
48
48
  @current_session = @current_selector = @max_concurrent_requests =
49
49
  @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
- @family = @io = @ssl_session = @timeout =
51
- @connected_at = @response_received_at = nil
50
+ @ping_timer = @family = @io = @ssl_session =
51
+ @timeout = @connected_at = @response_received_at = nil
52
52
 
53
53
  @exhausted = @cloned = @main_sibling = false
54
54
 
@@ -161,15 +161,17 @@ module HTTPX
161
161
  end
162
162
 
163
163
  def purge_pending(&block)
164
- pendings = []
165
164
  if @parser
166
165
  pending = @parser.pending
167
166
  @inflight -= pending.size
168
- pendings << pending
167
+ pending.reject! do |req|
168
+ block.call(req)
169
+ true
170
+ end
169
171
  end
170
- pendings << @pending
171
- pendings.each do |pending|
172
- pending.reject!(&block)
172
+ @pending.reject! do |req|
173
+ block.call(req)
174
+ true
173
175
  end
174
176
  end
175
177
 
@@ -203,7 +205,7 @@ module HTTPX
203
205
  return @parser.interests if @parser
204
206
 
205
207
  nil
206
- rescue StandardError => e
208
+ rescue Error => e
207
209
  on_error(e)
208
210
  nil
209
211
  end
@@ -215,6 +217,8 @@ module HTTPX
215
217
  def call
216
218
  case @state
217
219
  when :idle
220
+ return if no_more_requests?
221
+
218
222
  connect
219
223
 
220
224
  # when opening the tcp or ssl socket fails
@@ -222,18 +226,39 @@ module HTTPX
222
226
 
223
227
  consume
224
228
  when :closed
225
- return
229
+ return if no_more_requests?
230
+
231
+ # there are pending requests to send, restart the state machine.
232
+ idling
233
+
234
+ # @fiber-switch-guard
235
+ # fiber may have switch after ensuring that @io is closed.
236
+ return unless @state == :idle
237
+
238
+ call
226
239
  when :closing
227
240
  consume
228
241
  transition(:closed)
242
+
243
+ # @fiber-switch-guard
244
+ # fiber may have switch while closing @io.
245
+ return if @state == :closed &&
246
+ # only remain here if there are pending requests.
247
+ @pending.empty?
248
+
249
+ call
229
250
  when :open
230
251
  consume
231
252
  end
232
253
  nil
233
- rescue IOError => e
254
+ rescue Errno::ECONNRESET,
255
+ Errno::EINVAL,
256
+ SocketError,
257
+ IOError,
258
+ TLSError => e
234
259
  @write_buffer.clear
235
260
  on_io_error(e)
236
- rescue StandardError => e
261
+ rescue Error => e
237
262
  @write_buffer.clear
238
263
  on_error(e)
239
264
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -241,6 +266,10 @@ module HTTPX
241
266
  raise e
242
267
  end
243
268
 
269
+ def initial_call
270
+ call
271
+ end
272
+
244
273
  def close
245
274
  transition(:active) if @state == :inactive
246
275
 
@@ -251,7 +280,12 @@ module HTTPX
251
280
  case @state
252
281
  when :idle
253
282
  purge_after_closed
254
- disconnect
283
+
284
+ # @fiber-switch-guard
285
+ if @io.can_disconnect? && @pending.empty?
286
+ disconnect
287
+ return
288
+ end
255
289
  when :closed
256
290
  @connected_at = nil
257
291
  end
@@ -292,6 +326,11 @@ module HTTPX
292
326
  # (example: HTTP/1 parser disabling pipelining)
293
327
  return if @state == :idle && @pending.any?
294
328
 
329
+ if @ping_timer
330
+ @ping_timer.cancel
331
+ @ping_timer = nil
332
+ end
333
+
295
334
  parser = @parser
296
335
 
297
336
  if parser && parser.respond_to?(:max_concurrent_requests)
@@ -319,7 +358,7 @@ module HTTPX
319
358
  @pending << request
320
359
  transition(:active) if @state == :inactive
321
360
  request.ping!
322
- ping
361
+ ping(request)
323
362
  return
324
363
  end
325
364
 
@@ -341,6 +380,9 @@ module HTTPX
341
380
 
342
381
  def idling
343
382
  purge_after_closed
383
+
384
+ return unless @state == :closed
385
+
344
386
  @write_buffer.clear
345
387
  transition(:idle)
346
388
  return unless @parser
@@ -408,6 +450,11 @@ module HTTPX
408
450
 
409
451
  def on_io_error(e)
410
452
  on_error(e)
453
+
454
+ # do not force close if parser resets the connection.
455
+ # can happen i.e. when HTTP/1.1 pipelining is disabled.
456
+ return if @state == :idle && @pending.any?
457
+
411
458
  force_close(true)
412
459
  end
413
460
 
@@ -421,6 +468,8 @@ module HTTPX
421
468
  if @timeout
422
469
  @timeout -= error.timeout
423
470
  return unless @timeout <= 0
471
+
472
+ @timeout = nil
424
473
  end
425
474
 
426
475
  error = error.to_connection_error if connecting?
@@ -429,7 +478,7 @@ module HTTPX
429
478
  reset
430
479
  end
431
480
 
432
- # :nocov:
481
+ # simplecov:disable
433
482
  def inspect
434
483
  "#<#{self.class}:#{object_id} " \
435
484
  "@origin=#{@origin} " \
@@ -437,7 +486,7 @@ module HTTPX
437
486
  "@pending=#{@pending.size} " \
438
487
  "@io=#{@io}>"
439
488
  end
440
- # :nocov:
489
+ # simplecov:enable
441
490
 
442
491
  private
443
492
 
@@ -463,7 +512,7 @@ module HTTPX
463
512
  # * the number of pending requests
464
513
  # * the number of inflight requests
465
514
  # * whether the write buffer has bytes (i.e. for close handshake)
466
- if @pending.empty? && @inflight.zero? && @write_buffer.empty?
515
+ if no_more_requests? && @write_buffer.empty?
467
516
  no_more_requests_loop_check if @parser && @parser.pending.any?
468
517
 
469
518
  # terminate if an altsvc connection has been established
@@ -513,7 +562,7 @@ module HTTPX
513
562
  break if @state == :closing || @state == :closed
514
563
 
515
564
  # exit #consume altogether if all outstanding requests have been dealt with
516
- if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
565
+ if no_more_requests? && @write_buffer.empty? # rubocop:disable Style/Next
517
566
  no_more_requests_loop_check if @parser && @parser.pending.any?
518
567
 
519
568
  # terminate if an altsvc connection has been established
@@ -660,7 +709,12 @@ module HTTPX
660
709
  @exhausted = true
661
710
  parser.close
662
711
 
712
+ # @fiber-switch-guard
713
+ # fiber may have switched while closing @io, check whether still in the exhausted loop.
714
+ next unless @exhausted
715
+
663
716
  idling
717
+
664
718
  @exhausted = false
665
719
  end
666
720
  parser.on(:origin) do |origin|
@@ -676,6 +730,9 @@ module HTTPX
676
730
  enqueue_pending_requests_from_parser(parser)
677
731
 
678
732
  reset
733
+
734
+ next unless @state == :closed
735
+
679
736
  # :reset event only fired in http/1.1, so this guarantees
680
737
  # that the connection will be closed here.
681
738
  idling unless @pending.empty?
@@ -766,6 +823,9 @@ module HTTPX
766
823
  return unless @write_buffer.empty?
767
824
 
768
825
  purge_after_closed
826
+
827
+ # @fiber-switch-guard
828
+ return unless @state == :closing && (@io.nil? || @io.can_disconnect?)
769
829
  when :already_open
770
830
  nextstate = :open
771
831
  # the first check for given io readiness must still use a timeout.
@@ -833,7 +893,15 @@ module HTTPX
833
893
  end
834
894
 
835
895
  def purge_after_closed
836
- @io.close if @io
896
+ if @io
897
+ @io.close
898
+
899
+ # @fiber-switch-guard
900
+ # due to fiber scheduler, multiple fibers may be listening on the same connection
901
+ # and moving the state machine forward; in such cases, when the control flow reaches
902
+ # this line, the io object may not be closed anymore.
903
+ return unless @io&.can_disconnect?
904
+ end
837
905
  @read_buffer.clear
838
906
  @timeout = nil
839
907
  end
@@ -906,14 +974,25 @@ module HTTPX
906
974
  end
907
975
  end
908
976
 
909
- def ping
977
+ def ping(_request)
910
978
  return if parser.waiting_for_ping?
911
979
 
912
980
  parser.ping
981
+
982
+ ping_timeout = @options.timeout[:ping_timeout]
983
+
984
+ @ping_timer = @current_selector.after(ping_timeout) do
985
+ log(level: 3) { "ping timeout expired..." }
986
+ error = PingTimeoutError.new(ping_timeout, "Timed out after #{ping_timeout} seconds")
987
+ on_error(error)
988
+ end
989
+
913
990
  call
914
991
  end
915
992
 
916
993
  def pong
994
+ @ping_timer.cancel
995
+ @ping_timer = nil
917
996
  @response_received_at = Utils.now
918
997
  @no_more_requests_counter = 0
919
998
  send_pending
@@ -930,6 +1009,11 @@ module HTTPX
930
1009
  "along with debug logs"
931
1010
  end
932
1011
 
1012
+ # true when there are no more pending nor inflight (in parser) requests
1013
+ def no_more_requests?
1014
+ @pending.empty? && @inflight.zero?
1015
+ end
1016
+
933
1017
  # recover internal state and emit all relevant error responses when +error+ was raised.
934
1018
  # this takes an optiona +request+ which may have already been handled and can be opted out
935
1019
  # in the state recovery process.
@@ -964,6 +1048,7 @@ module HTTPX
964
1048
  set_request_write_timeout(request)
965
1049
  set_request_read_timeout(request)
966
1050
  set_request_request_timeout(request)
1051
+ set_request_total_request_timeout(request)
967
1052
  end
968
1053
 
969
1054
  def set_request_read_timeout(request)
@@ -1016,6 +1101,18 @@ module HTTPX
1016
1101
  request.handle_error(error)
1017
1102
  end
1018
1103
 
1104
+ def set_request_total_request_timeout(request)
1105
+ return if request.started?
1106
+
1107
+ total_request_timeout = request.total_request_timeout
1108
+
1109
+ return if total_request_timeout.nil? || total_request_timeout.infinite?
1110
+
1111
+ set_request_timeout(:total_request_timeout, request, total_request_timeout, :headers, :complete) do
1112
+ read_timeout_callback(request, total_request_timeout, TotalRequestTimeoutError)
1113
+ end
1114
+ end
1115
+
1019
1116
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
1020
1117
  request.set_timeout_callback(start_event) do
1021
1118
  unless (selector = @current_selector)
@@ -1030,13 +1127,14 @@ module HTTPX
1030
1127
  end
1031
1128
 
1032
1129
  timer = selector.after(timeout, callback)
1033
- request.active_timeouts << label
1130
+ timer.label = label
1131
+ request.active_timeouts << timer
1034
1132
 
1035
1133
  Array(finish_events).each do |event|
1036
1134
  # clean up request timeouts if the connection errors out
1037
1135
  request.set_timeout_callback(event) do
1038
1136
  timer.cancel
1039
- request.active_timeouts.delete(label)
1137
+ request.active_timeouts.delete(timer)
1040
1138
  end
1041
1139
  end
1042
1140
  end
data/lib/httpx/errors.rb CHANGED
@@ -62,15 +62,22 @@ module HTTPX
62
62
  # Error raised when there was a timeout while sending a request from the server.
63
63
  class WriteTimeoutError < RequestTimeoutError; end
64
64
 
65
+ # Error raised when a response couldn't be received for a request after multiple interactions.
66
+ # This error should not be retriable.
67
+ class TotalRequestTimeoutError < RequestTimeoutError; end
68
+
65
69
  # Error raised when there was a timeout while waiting for the HTTP/2 settings frame from the server.
66
70
  class SettingsTimeoutError < TimeoutError; end
67
71
 
68
72
  # Error raised when there was a timeout while resolving a domain to an IP.
69
73
  class ResolveTimeoutError < TimeoutError; end
70
74
 
71
- # Error raise when there was a timeout waiting for readiness of the socket the request is related to.
75
+ # Error raised when there was a timeout waiting for readiness of the socket the request is related to.
72
76
  class OperationTimeoutError < TimeoutError; end
73
77
 
78
+ # Error raised when a connection liveness probe (aka ping) times out.
79
+ class PingTimeoutError < TimeoutError; end
80
+
74
81
  # Error raised when there was an error while resolving a domain to an IP.
75
82
  class ResolveError < Error; end
76
83
 
data/lib/httpx/headers.rb CHANGED
@@ -140,12 +140,12 @@ module HTTPX
140
140
  @headers.to_s
141
141
  end
142
142
 
143
- # :nocov:
143
+ # simplecov:disable
144
144
  def inspect
145
145
  "#<#{self.class}:#{object_id} " \
146
146
  "#{to_hash.inspect}>"
147
147
  end
148
- # :nocov:
148
+ # simplecov:enable
149
149
 
150
150
  # this is internal API and doesn't abide to other public API
151
151
  # guarantees, like downcasing strings.
data/lib/httpx/io/ssl.rb CHANGED
@@ -20,8 +20,9 @@ module HTTPX
20
20
  super
21
21
 
22
22
  @ssl_session = nil
23
- ctx_options = TLS_OPTIONS.merge(options.ssl)
24
- @sni_hostname = ctx_options.delete(:hostname) || @hostname
23
+ ctx_options = TLS_OPTIONS
24
+ ctx_options = ctx_options.merge(options.ssl) if options.ssl && !options.ssl.empty?
25
+ @sni_hostname = (ctx_options.delete(:hostname) if ctx_options.key?(:hostname)) || @hostname
25
26
 
26
27
  if @keep_open && @io.is_a?(OpenSSL::SSL::SSLSocket)
27
28
  # externally initiated ssl socket
@@ -29,7 +30,7 @@ module HTTPX
29
30
  @state = :negotiated
30
31
  else
31
32
  @ctx = OpenSSL::SSL::SSLContext.new
32
- @ctx.set_params(ctx_options) unless ctx_options.empty?
33
+ @ctx.set_params(ctx_options)
33
34
  unless @ctx.session_cache_mode.nil? # a dummy method on JRuby
34
35
  @ctx.session_cache_mode =
35
36
  OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
@@ -51,24 +52,22 @@ module HTTPX
51
52
  end
52
53
 
53
54
  def protocol
54
- # @type ivar @io: OpenSSL::SSL::SSLSocket
55
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
56
+
55
57
  @io.alpn_protocol || super
56
- rescue StandardError
57
- super
58
58
  end
59
59
 
60
60
  if RUBY_ENGINE == "jruby"
61
61
  # in jruby, alpn_protocol may return ""
62
62
  # https://github.com/jruby/jruby-openssl/issues/287
63
63
  def protocol
64
- # @type ivar @io: OpenSSL::SSL::SSLSocket
64
+ return super unless @io.is_a?(OpenSSL::SSL::SSLSocket)
65
+
65
66
  proto = @io.alpn_protocol
66
67
 
67
68
  return super if proto.nil? || proto.empty?
68
69
 
69
70
  proto
70
- rescue StandardError
71
- super
72
71
  end
73
72
  end
74
73
 
data/lib/httpx/io/tcp.rb CHANGED
@@ -182,18 +182,35 @@ module HTTPX
182
182
  end
183
183
 
184
184
  def close
185
- return if @keep_open || closed?
185
+ return if @keep_open
186
+
187
+ # mark tcp as closed, so that it can be disconnected.
188
+ # this bypasses the state machine API as not not allow the transition
189
+ # from idle to closed in normal circumstances.
190
+ @state = :closed if @state == :idle
191
+
192
+ return if closed?
186
193
 
187
194
  begin
188
195
  @io.close
189
- rescue StandardError => e
196
+ rescue IOError => e
190
197
  log { "error closing socket" }
191
198
  log { e.full_message(highlight: false) }
192
199
  ensure
193
- transition(:closed)
200
+ # @fiber-switch-guard
201
+ # ensure that all :closed IOs don't leave dangling sockets
202
+ # behind. This may happen in a fiber scheduler scenario where
203
+ # connection is reused across fibers.
204
+ transition(:closed) if @io.closed?
194
205
  end
195
206
  end
196
207
 
208
+ # signals that the connection that contains this IO can be checked back into the pool.
209
+ # that includes sockets opened outside of the scope of the session, or closed IOs.
210
+ def can_disconnect?
211
+ @keep_open || @state == :closed
212
+ end
213
+
197
214
  def connected?
198
215
  @state == :connected
199
216
  end
@@ -202,7 +219,7 @@ module HTTPX
202
219
  @state == :idle || @state == :closed
203
220
  end
204
221
 
205
- # :nocov:
222
+ # simplecov:disable
206
223
  def inspect
207
224
  "#<#{self.class}:#{object_id} " \
208
225
  "#{@ip}:#{@port} " \
@@ -211,7 +228,7 @@ module HTTPX
211
228
  "@addresses=#{@addresses} " \
212
229
  "@state=#{@state}>"
213
230
  end
214
- # :nocov:
231
+ # simplecov:enable
215
232
 
216
233
  private
217
234
 
data/lib/httpx/io/unix.rb CHANGED
@@ -60,11 +60,11 @@ module HTTPX
60
60
  true
61
61
  end
62
62
 
63
- # :nocov:
63
+ # simplecov:disable
64
64
  def inspect
65
65
  "#<#{self.class}:#{object_id} @path=#{@path}) @state=#{@state})>"
66
66
  end
67
- # :nocov:
67
+ # simplecov:enable
68
68
 
69
69
  private
70
70
 
@@ -17,6 +17,12 @@ module HTTPX
17
17
 
18
18
  USE_DEBUG_LOG = ENV.key?("HTTPX_DEBUG")
19
19
 
20
+ def self.log_identifiers
21
+ "pid=#{Process.pid} " \
22
+ "tid=#{Thread.current.object_id} " \
23
+ "fid=#{Fiber.current.object_id}"
24
+ end
25
+
20
26
  def log(
21
27
  level: @options.debug_level,
22
28
  color: nil,
@@ -36,10 +42,9 @@ module HTTPX
36
42
  klass = klass.superclass
37
43
  end
38
44
 
39
- message = +"(time:#{Time.now.utc}, pid:#{Process.pid}, " \
40
- "tid:#{Thread.current.object_id}, " \
41
- "fid:#{Fiber.current.object_id}, " \
42
- "self:#{class_name}##{object_id}) "
45
+ message = +"(time=#{Time.now.utc} " \
46
+ "#{Loggable.log_identifiers} " \
47
+ "self=#{class_name}##{object_id}) "
43
48
  message << msg.call << "\n"
44
49
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
45
50
  debug_stream << message
@@ -49,6 +54,8 @@ module HTTPX
49
54
  log(level: level, color: color, debug_level: debug_level, debug: debug) { ex.full_message }
50
55
  end
51
56
 
57
+ private
58
+
52
59
  def log_redact_headers(text)
53
60
  log_redact(text, @options.debug_redact == :headers)
54
61
  end
data/lib/httpx/options.rb CHANGED
@@ -8,12 +8,12 @@ module HTTPX
8
8
  WINDOW_SIZE = 1 << 14 # 16K
9
9
  MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
10
10
  KEEP_ALIVE_TIMEOUT = 20
11
+ PING_TIMEOUT = 2
11
12
  SETTINGS_TIMEOUT = 10
12
13
  CLOSE_HANDSHAKE_TIMEOUT = 10
13
14
  CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
14
- REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
15
+ REQUEST_TIMEOUT = OPERATION_TIMEOUT = TOTAL_REQUEST_TIMEOUT = nil
15
16
  RESOLVER_TYPES = %i[memory file].freeze
16
-
17
17
  # default value used for "user-agent" header, when not overridden.
18
18
  USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
19
19
 
@@ -79,9 +79,15 @@ module HTTPX
79
79
  # :decompress_response_body :: whether to auto-decompress response body (defaults to <tt>true</tt>).
80
80
  # :compress_request_body :: whether to auto-decompress response body (defaults to <tt>true</tt>)
81
81
  # :timeout :: hash of timeout configurations (supports <tt>:connect_timeout</tt>, <tt>:settings_timeout</tt>,
82
- # <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>, <tt>:read_timeout</tt>, <tt>:write_timeout</tt>
83
- # and <tt>:request_timeout</tt>
82
+ # <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>, <tt>:read_timeout</tt>, <tt>:write_timeout</tt>,
83
+ # <tt>:request_timeout</tt>, <tt>:total_request_timeout</tt> and <tt>:ping_timeout</tt>,
84
84
  # :headers :: hash of HTTP headers (ex: <tt>{ "x-custom-foo" => "bar" }</tt>)
85
+ # :max_response_body_size :: maximum size (in bytes) that the response body can consume (no threshold by default), after which an
86
+ # error is raised.
87
+ # :max_response_headers :: maximum number of header fields that a response can receive, after which an error is raised.
88
+ # :max_response_header_value_size :: maximum size (in bytes) a header value can have (no threshold by default).
89
+ # for cases where the value is broken into multiple header fields (such as "cookie" or "set-cookie"),
90
+ # this is the total aggregated size.
85
91
  # :window_size :: number of bytes to read from a socket
86
92
  # :buffer_size :: internal read and write buffer size in bytes
87
93
  # :body_threshold_size :: maximum size in bytes of response payload that is buffered in memory.
@@ -387,6 +393,7 @@ module HTTPX
387
393
  # number options
388
394
  %i[
389
395
  max_concurrent_requests max_requests window_size buffer_size
396
+ max_response_body_size max_response_headers max_response_header_value_size
390
397
  body_threshold_size debug_level
391
398
  ].each do |option|
392
399
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
@@ -553,15 +560,20 @@ module HTTPX
553
560
  :supported_compression_formats => %w[gzip deflate],
554
561
  :decompress_response_body => true,
555
562
  :compress_request_body => true,
563
+ :max_response_headers => 1000,
564
+ :max_response_header_value_size => nil,
565
+ :max_response_body_size => Float::INFINITY,
556
566
  :timeout => {
557
567
  connect_timeout: CONNECT_TIMEOUT,
558
568
  settings_timeout: SETTINGS_TIMEOUT,
559
569
  close_handshake_timeout: CLOSE_HANDSHAKE_TIMEOUT,
560
570
  operation_timeout: OPERATION_TIMEOUT,
561
571
  keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
572
+ ping_timeout: PING_TIMEOUT,
562
573
  read_timeout: READ_TIMEOUT,
563
574
  write_timeout: WRITE_TIMEOUT,
564
575
  request_timeout: REQUEST_TIMEOUT,
576
+ total_request_timeout: TOTAL_REQUEST_TIMEOUT,
565
577
  }.freeze,
566
578
  :headers_class => Class.new(Headers, &SET_TEMPORARY_NAME),
567
579
  :headers => EMPTY_HASH,
@@ -9,11 +9,13 @@ module HTTPX
9
9
 
10
10
  attr_reader :status_code, :http_version, :headers
11
11
 
12
- def initialize(observer)
12
+ def initialize(observer, max_headers, max_header_value_size)
13
13
  @observer = observer
14
14
  @state = :idle
15
15
  @buffer = "".b
16
16
  @headers = {}
17
+ @max_headers = max_headers
18
+ @max_header_value_size = max_header_value_size
17
19
  @content_length = nil
18
20
  @_has_trailers = @upgrade = false
19
21
  end
@@ -117,7 +119,11 @@ module HTTPX
117
119
  value.strip!
118
120
  raise Error, "wrong header format" if value.nil?
119
121
 
120
- (headers[key.downcase] ||= []) << value
122
+ values = (headers[key.downcase] ||= []) << value
123
+
124
+ raise Error, "maximum header value size exceeded" if @max_header_value_size && (values.sum(&:size) > @max_header_value_size)
125
+
126
+ raise Error, "maximum number of response headers exceeded" if headers.size > @max_headers
121
127
  end
122
128
  end
123
129
 
@@ -24,7 +24,7 @@ module HTTPX
24
24
 
25
25
  def authenticate(request, authenticate)
26
26
  "Digest #{generate_header(request.verb, request.path, authenticate)}"
27
- rescue StandardError => e
27
+ rescue HTTPX::Error => e
28
28
  response = ErrorResponse.new(request, e)
29
29
  request.response = response
30
30
  request.emit_response(response)