httpx 1.7.8 → 1.8.0

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_0.md +100 -0
  3. data/lib/httpx/adapters/datadog.rb +3 -1
  4. data/lib/httpx/connection/http1.rb +10 -1
  5. data/lib/httpx/connection/http2.rb +37 -4
  6. data/lib/httpx/connection.rb +76 -7
  7. data/lib/httpx/errors.rb +8 -1
  8. data/lib/httpx/io/tcp.rb +11 -1
  9. data/lib/httpx/options.rb +16 -4
  10. data/lib/httpx/parser/http1.rb +8 -2
  11. data/lib/httpx/plugins/auth.rb +52 -4
  12. data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
  13. data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
  14. data/lib/httpx/plugins/cache.rb +221 -0
  15. data/lib/httpx/plugins/fiber_concurrency.rb +50 -3
  16. data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
  17. data/lib/httpx/plugins/oauth.rb +66 -14
  18. data/lib/httpx/plugins/proxy.rb +5 -0
  19. data/lib/httpx/plugins/response_cache.rb +26 -105
  20. data/lib/httpx/plugins/retries.rb +7 -5
  21. data/lib/httpx/plugins/server_sent_events.rb +158 -0
  22. data/lib/httpx/plugins/ssrf_filter.rb +16 -1
  23. data/lib/httpx/plugins/stream.rb +7 -3
  24. data/lib/httpx/plugins/tracing.rb +15 -4
  25. data/lib/httpx/request.rb +18 -1
  26. data/lib/httpx/resolver/cache/file.rb +56 -0
  27. data/lib/httpx/resolver/native.rb +14 -3
  28. data/lib/httpx/response/body.rb +4 -2
  29. data/lib/httpx/response.rb +9 -1
  30. data/lib/httpx/selector.rb +7 -1
  31. data/lib/httpx/version.rb +1 -1
  32. data/sig/chainable.rbs +3 -0
  33. data/sig/connection/http1.rbs +1 -1
  34. data/sig/connection/http2.rbs +1 -1
  35. data/sig/connection.rbs +11 -8
  36. data/sig/errors.rbs +9 -3
  37. data/sig/httpx.rbs +2 -0
  38. data/sig/io/tcp.rbs +2 -0
  39. data/sig/loggable.rbs +4 -0
  40. data/sig/options.rbs +25 -12
  41. data/sig/parser/http1.rbs +3 -1
  42. data/sig/plugins/auth/ntlm.rbs +1 -1
  43. data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
  44. data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
  45. data/sig/plugins/cache.rbs +69 -0
  46. data/sig/plugins/fiber_concurrency.rbs +4 -0
  47. data/sig/plugins/ntlm_v2_auth.rbs +36 -0
  48. data/sig/plugins/response_cache.rbs +13 -38
  49. data/sig/plugins/retries.rbs +5 -5
  50. data/sig/plugins/server_sent_events.rbs +45 -0
  51. data/sig/plugins/ssrf_filter.rbs +5 -1
  52. data/sig/plugins/stream.rbs +1 -1
  53. data/sig/plugins/stream_bidi.rbs +0 -2
  54. data/sig/plugins/webdav.rbs +1 -1
  55. data/sig/pool.rbs +2 -2
  56. data/sig/request.rbs +7 -3
  57. data/sig/resolver/cache/file.rbs +13 -0
  58. data/sig/resolver/entry.rbs +1 -1
  59. data/sig/resolver/https.rbs +3 -3
  60. data/sig/resolver/multi.rbs +1 -1
  61. data/sig/resolver/native.rbs +5 -5
  62. data/sig/resolver/resolver.rbs +1 -3
  63. data/sig/resolver/system.rbs +2 -2
  64. data/sig/resolver.rbs +3 -0
  65. data/sig/response.rbs +3 -0
  66. data/sig/selector.rbs +11 -8
  67. data/sig/timers.rbs +5 -5
  68. data/sig/transcoder/body.rbs +1 -1
  69. data/sig/transcoder/gzip.rbs +3 -2
  70. data/sig/transcoder/multipart.rbs +4 -1
  71. data/sig/transcoder/utils/deflater.rbs +2 -0
  72. data/sig/transcoder.rbs +2 -0
  73. data/sig/utils.rbs +1 -1
  74. metadata +17 -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: 74104ce6c4d05053e2a005974de5f0cd66346a1cb4558b7c0887d3a2d691d226
4
+ data.tar.gz: ca0ce3e30f509d44e4c0daac30a0b5d9949c118c19a715561c276f1debaf5dbf
5
5
  SHA512:
6
- metadata.gz: 2c52b06329f4d13d95242ec6043cf615e077a6dcdef6ad3cfe06d8893eb27555df4d5cdbcd95b621f9d43b4c40281becfbdb4005a074ea34463ca6e951537363
7
- data.tar.gz: 4060403c67050b2d9603ec801330d0e32d24c86bf10094ce396e646ab2649870981df0eb66f042a5da1835ba349ef69024148458b6fa3e23657ae2c1a8bc7cfc
6
+ metadata.gz: 0de3c6baf56c86272cc45bdc06e1187655e759ddf329414d71fcbf4603c9d0e03791641e05f5c07ef84cd578a3bba1a855fde77b5b0f1f2e11df1cf60ea0815a
7
+ data.tar.gz: a276c6ed937c8d04103382d517d56d9df805886f23b05a38f6e654fcd6935e17407101e1eedc2b6605b4f347a37262116024a61633ebdd660131b06fc1f3fde9
@@ -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.
@@ -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
@@ -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
@@ -118,7 +118,7 @@ module HTTPX
118
118
  return false
119
119
  end
120
120
  unless (stream = @streams[request])
121
- stream = @connection.new_stream
121
+ stream = @connection.new_stream(**request.http2_stream_options)
122
122
  handle_stream(stream, request)
123
123
  @streams[request] = stream
124
124
  @max_requests -= 1
@@ -128,6 +128,8 @@ module HTTPX
128
128
  rescue ::HTTP2::Error::StreamLimitExceeded
129
129
  @pending.unshift(request)
130
130
  false
131
+ rescue StandardError => e
132
+ emit(:error, request, e)
131
133
  end
132
134
 
133
135
  def consume
@@ -312,7 +314,21 @@ module HTTPX
312
314
  end
313
315
  _, status = h.shift
314
316
  headers = request.options.headers_class.new(h)
317
+
318
+ raise HTTPX::Error, "maximum number of response headers exceeded" if h.size > @options.max_response_headers
319
+
320
+ if (max_header_value_size = @options.max_response_header_value_size)
321
+ headers.each do |_, v| # rubocop:disable Style/HashEachMethods
322
+ raise HTTPX::Error, "maximum header value size exceeded" if v.size > max_header_value_size
323
+ end
324
+ end
325
+
315
326
  response = request.options.response_class.new(request, status, "2.0", headers)
327
+
328
+ if response.content_length && response.content_length > request.options.max_response_body_size
329
+ raise HTTPX::Error.new, "maximum response body size exceeded"
330
+ end
331
+
316
332
  request.response = response
317
333
  @streams[request] = stream
318
334
 
@@ -428,11 +444,28 @@ module HTTPX
428
444
  end
429
445
 
430
446
  def frame_with_extra_info(frame)
447
+ flags_bits = frame.fetch(:flags, 0)
431
448
  case frame[:type]
432
449
  when :data
433
- frame.merge(payload: frame[:payload].bytesize)
434
- when :headers, :ping
435
- frame.merge(payload: log_redact_headers(frame[:payload]))
450
+ flags = [] #: Array[Symbol]
451
+ flags << :end_stream if flags_bits.anybits?(0b0001)
452
+ flags << :padded if flags_bits.anybits?(0b1000)
453
+ frame.merge(payload: frame[:payload].bytesize, flags: flags)
454
+ when :push_promise, :headers
455
+ flags = [] #: Array[Symbol]
456
+ flags << :end_stream if flags_bits.anybits?(0b0001)
457
+ flags << :priority if flags_bits.anybits?(0b0010)
458
+ flags << :end_headers if flags_bits.anybits?(0b0100)
459
+ flags << :padded if flags_bits.anybits?(0b1000)
460
+ frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
461
+ when :ping
462
+ flags = [] #: Array[Symbol]
463
+ flags << :ack if flags_bits.anybits?(0b0001)
464
+ frame.merge(payload: log_redact_headers(frame[:payload]), flags: flags)
465
+ when :settings
466
+ flags = [] #: Array[Symbol]
467
+ flags << :ack if flags_bits.anybits?(0b0001)
468
+ frame.merge(flags: flags)
436
469
  when :window_update
437
470
  connection_or_stream = if (id = frame[:stream]).zero?
438
471
  @connection
@@ -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
 
@@ -222,10 +222,27 @@ module HTTPX
222
222
 
223
223
  consume
224
224
  when :closed
225
- return
225
+ return if @pending.empty?
226
+
227
+ # there are pending requests to send, restart the state machine.
228
+ idling
229
+
230
+ # @fiber-switch-guard
231
+ # fiber may have switch after ensuring that @io is closed.
232
+ return unless @state == :idle
233
+
234
+ call
226
235
  when :closing
227
236
  consume
228
237
  transition(:closed)
238
+
239
+ # @fiber-switch-guard
240
+ # fiber may have switch while closing @io.
241
+ return if @state == :closed &&
242
+ # only remain here if there are pending requests.
243
+ @pending.empty?
244
+
245
+ call
229
246
  when :open
230
247
  consume
231
248
  end
@@ -251,7 +268,12 @@ module HTTPX
251
268
  case @state
252
269
  when :idle
253
270
  purge_after_closed
254
- disconnect
271
+
272
+ # @fiber-switch-guard
273
+ if @io.can_disconnect? && @pending.empty?
274
+ disconnect
275
+ return
276
+ end
255
277
  when :closed
256
278
  @connected_at = nil
257
279
  end
@@ -319,7 +341,7 @@ module HTTPX
319
341
  @pending << request
320
342
  transition(:active) if @state == :inactive
321
343
  request.ping!
322
- ping
344
+ ping(request)
323
345
  return
324
346
  end
325
347
 
@@ -341,6 +363,9 @@ module HTTPX
341
363
 
342
364
  def idling
343
365
  purge_after_closed
366
+
367
+ return unless @state == :closed
368
+
344
369
  @write_buffer.clear
345
370
  transition(:idle)
346
371
  return unless @parser
@@ -421,6 +446,8 @@ module HTTPX
421
446
  if @timeout
422
447
  @timeout -= error.timeout
423
448
  return unless @timeout <= 0
449
+
450
+ @timeout = nil
424
451
  end
425
452
 
426
453
  error = error.to_connection_error if connecting?
@@ -660,7 +687,12 @@ module HTTPX
660
687
  @exhausted = true
661
688
  parser.close
662
689
 
690
+ # @fiber-switch-guard
691
+ # fiber may have switched while closing @io, check whether still in the exhausted loop.
692
+ next unless @exhausted
693
+
663
694
  idling
695
+
664
696
  @exhausted = false
665
697
  end
666
698
  parser.on(:origin) do |origin|
@@ -676,6 +708,9 @@ module HTTPX
676
708
  enqueue_pending_requests_from_parser(parser)
677
709
 
678
710
  reset
711
+
712
+ next unless @state == :closed
713
+
679
714
  # :reset event only fired in http/1.1, so this guarantees
680
715
  # that the connection will be closed here.
681
716
  idling unless @pending.empty?
@@ -766,6 +801,9 @@ module HTTPX
766
801
  return unless @write_buffer.empty?
767
802
 
768
803
  purge_after_closed
804
+
805
+ # @fiber-switch-guard
806
+ return unless @state == :closing && (@io.nil? || @io.can_disconnect?)
769
807
  when :already_open
770
808
  nextstate = :open
771
809
  # the first check for given io readiness must still use a timeout.
@@ -833,7 +871,15 @@ module HTTPX
833
871
  end
834
872
 
835
873
  def purge_after_closed
836
- @io.close if @io
874
+ if @io
875
+ @io.close
876
+
877
+ # @fiber-switch-guard
878
+ # due to fiber scheduler, multiple fibers may be listening on the same connection
879
+ # and moving the state machine forward; in such cases, when the control flow reaches
880
+ # this line, the io object may not be closed anymore.
881
+ return unless @io&.can_disconnect?
882
+ end
837
883
  @read_buffer.clear
838
884
  @timeout = nil
839
885
  end
@@ -906,14 +952,24 @@ module HTTPX
906
952
  end
907
953
  end
908
954
 
909
- def ping
955
+ def ping(_request)
910
956
  return if parser.waiting_for_ping?
911
957
 
912
958
  parser.ping
959
+
960
+ ping_timeout = @options.timeout[:ping_timeout]
961
+
962
+ @ping_timer = @current_selector.after(ping_timeout) do
963
+ error = PingTimeoutError.new(ping_timeout, "Timed out after #{ping_timeout} seconds")
964
+ on_error(error)
965
+ end
966
+
913
967
  call
914
968
  end
915
969
 
916
970
  def pong
971
+ @ping_timer.cancel
972
+ @ping_timer = nil
917
973
  @response_received_at = Utils.now
918
974
  @no_more_requests_counter = 0
919
975
  send_pending
@@ -964,6 +1020,7 @@ module HTTPX
964
1020
  set_request_write_timeout(request)
965
1021
  set_request_read_timeout(request)
966
1022
  set_request_request_timeout(request)
1023
+ set_request_total_request_timeout(request)
967
1024
  end
968
1025
 
969
1026
  def set_request_read_timeout(request)
@@ -1016,6 +1073,18 @@ module HTTPX
1016
1073
  request.handle_error(error)
1017
1074
  end
1018
1075
 
1076
+ def set_request_total_request_timeout(request)
1077
+ return if request.started?
1078
+
1079
+ total_request_timeout = request.total_request_timeout
1080
+
1081
+ return if total_request_timeout.nil? || total_request_timeout.infinite?
1082
+
1083
+ set_request_timeout(:total_request_timeout, request, total_request_timeout, :headers, :complete) do
1084
+ read_timeout_callback(request, total_request_timeout, TotalRequestTimeoutError)
1085
+ end
1086
+ end
1087
+
1019
1088
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
1020
1089
  request.set_timeout_callback(start_event) do
1021
1090
  unless (selector = @current_selector)
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/io/tcp.rb CHANGED
@@ -190,10 +190,20 @@ module HTTPX
190
190
  log { "error closing socket" }
191
191
  log { e.full_message(highlight: false) }
192
192
  ensure
193
- transition(:closed)
193
+ # @fiber-switch-guard
194
+ # ensure that all :closed IOs don't leave dangling sockets
195
+ # behind. This may happen in a fiber scheduler scenario where
196
+ # connection is reused across fibers.
197
+ transition(:closed) if @io.closed?
194
198
  end
195
199
  end
196
200
 
201
+ # signals that the connection that contains this IO can be checked back into the pool.
202
+ # that includes sockets opened outside of the scope of the session, or closed IOs.
203
+ def can_disconnect?
204
+ @keep_open || @state == :closed
205
+ end
206
+
197
207
  def connected?
198
208
  @state == :connected
199
209
  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
 
@@ -20,6 +20,9 @@ module HTTPX
20
20
  #
21
21
  # :auth_header_value :: the token to use as a string, or a callable which returns a string when called.
22
22
  # :auth_header_type :: the authentication type to use in the "authorization" header value (i.e. "Bearer", "Digest"...)
23
+ # :auth_header_expires_at :: timestamp at which the auth header will be discarded. should be a callable (like a proc)
24
+ # receiving the request as an argument, and should return either a Time object, or an integer (UNIX time).
25
+ # :auth_header_expires_in :: time (in seconds) since the first use of an auth header after which that header will be discarded.
23
26
  # :generate_auth_value_on_retry :: callable which returns whether the request should regenerate the auth_header_value
24
27
  # when the request is retried (this option will only work if the session also loads the
25
28
  # <tt>:retries</tt> plugin).
@@ -32,6 +35,22 @@ module HTTPX
32
35
  value
33
36
  end
34
37
 
38
+ def option_auth_header_expires_at(value)
39
+ unless value.respond_to?(:call)
40
+ value = Float(value)
41
+ raise TypeError, "`:auth_header_expires_at` must be positive" unless value.positive?
42
+ end
43
+
44
+ value
45
+ end
46
+
47
+ def option_auth_header_expires_in(value)
48
+ value = Float(value)
49
+ raise TypeError, "`:auth_header_expires_in` must be positive" unless value.positive?
50
+
51
+ value
52
+ end
53
+
35
54
  def option_generate_auth_value_on_retry(value)
36
55
  raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
37
56
 
@@ -43,7 +62,7 @@ module HTTPX
43
62
  def initialize(*)
44
63
  super
45
64
 
46
- @auth_header_value = nil
65
+ @auth_header_value = @auth_header_expires_at = nil
47
66
  @auth_header_value_mtx = Thread::Mutex.new
48
67
  @skip_auth_header_value = false
49
68
  end
@@ -65,7 +84,7 @@ module HTTPX
65
84
 
66
85
  def reset_auth_header_value!
67
86
  @auth_header_value_mtx.synchronize do
68
- @auth_header_value = nil
87
+ @auth_header_value = @auth_header_expires_at = nil
69
88
  end
70
89
  end
71
90
 
@@ -75,7 +94,12 @@ module HTTPX
75
94
  return super if @skip_auth_header_value || request.authorized?
76
95
 
77
96
  auth_header_value = @auth_header_value_mtx.synchronize do
78
- @auth_header_value ||= generate_auth_token
97
+ try_invalidate_auth_header_value
98
+
99
+ @auth_header_value ||= begin
100
+ set_auth_header_expires_at(request)
101
+ generate_auth_token
102
+ end
79
103
  end
80
104
 
81
105
  request.authorize(auth_header_value) if auth_header_value
@@ -83,6 +107,14 @@ module HTTPX
83
107
  super
84
108
  end
85
109
 
110
+ def try_invalidate_auth_header_value
111
+ return unless (expires_at = @auth_header_expires_at)
112
+
113
+ return if expires_at > Time.now.utc.to_i
114
+
115
+ @auth_header_value = @auth_header_expires_at = nil
116
+ end
117
+
86
118
  def generate_auth_token
87
119
  return unless (auth_value = @options.auth_header_value)
88
120
 
@@ -91,6 +123,19 @@ module HTTPX
91
123
  auth_value
92
124
  end
93
125
 
126
+ def set_auth_header_expires_at(request)
127
+ @auth_header_expires_at = if (expires_in = request.options.auth_header_expires_in)
128
+ Time.now.to_i + expires_in
129
+ elsif (expires_at = request.options.auth_header_expires_at)
130
+ if expires_at.respond_to?(:call)
131
+ expires_at = expires_at.call(request).to_f
132
+ raise Error, "`:auth_header_expires_at` must be positive" unless expires_at.positive?
133
+
134
+ expires_at
135
+ end
136
+ end
137
+ end
138
+
94
139
  def dynamic_auth_token?(auth_header_value)
95
140
  auth_header_value&.respond_to?(:call)
96
141
  end
@@ -150,7 +195,10 @@ module HTTPX
150
195
  # otherwise, it means that the first request already passed here, so this request should
151
196
  # use whatever was generated for it.
152
197
  @auth_header_value_mtx.synchronize do
153
- @auth_header_value = generate_auth_token if request.auth_token_value == @auth_header_value
198
+ if request.auth_token_value == @auth_header_value
199
+ @auth_header_value = generate_auth_token
200
+ set_auth_header_expires_at(request)
201
+ end
154
202
  end
155
203
 
156
204
  request.unauthorize!
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module HTTPX::Plugins
6
- module ResponseCache
6
+ module Cache
7
7
  # Implementation of a file system based cache store.
8
8
  #
9
9
  # It stores cached responses in a file under a directory pointed by the +dir+