httpx 1.2.6 → 1.4.4

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_3_0.md +18 -0
  4. data/doc/release_notes/1_3_1.md +17 -0
  5. data/doc/release_notes/1_3_2.md +6 -0
  6. data/doc/release_notes/1_3_3.md +5 -0
  7. data/doc/release_notes/1_3_4.md +6 -0
  8. data/doc/release_notes/1_4_0.md +43 -0
  9. data/doc/release_notes/1_4_1.md +19 -0
  10. data/doc/release_notes/1_4_2.md +20 -0
  11. data/doc/release_notes/1_4_3.md +11 -0
  12. data/doc/release_notes/1_4_4.md +14 -0
  13. data/lib/httpx/adapters/datadog.rb +56 -80
  14. data/lib/httpx/adapters/faraday.rb +5 -2
  15. data/lib/httpx/adapters/webmock.rb +24 -8
  16. data/lib/httpx/callbacks.rb +2 -7
  17. data/lib/httpx/chainable.rb +3 -1
  18. data/lib/httpx/connection/http1.rb +11 -7
  19. data/lib/httpx/connection/http2.rb +57 -34
  20. data/lib/httpx/connection.rb +270 -71
  21. data/lib/httpx/errors.rb +15 -4
  22. data/lib/httpx/io/ssl.rb +6 -3
  23. data/lib/httpx/io/tcp.rb +1 -1
  24. data/lib/httpx/io/unix.rb +1 -1
  25. data/lib/httpx/loggable.rb +17 -10
  26. data/lib/httpx/options.rb +30 -23
  27. data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
  28. data/lib/httpx/plugins/aws_sigv4.rb +36 -17
  29. data/lib/httpx/plugins/callbacks.rb +13 -2
  30. data/lib/httpx/plugins/circuit_breaker.rb +11 -5
  31. data/lib/httpx/plugins/content_digest.rb +202 -0
  32. data/lib/httpx/plugins/cookies.rb +9 -6
  33. data/lib/httpx/plugins/digest_auth.rb +3 -0
  34. data/lib/httpx/plugins/expect.rb +10 -4
  35. data/lib/httpx/plugins/follow_redirects.rb +68 -33
  36. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  37. data/lib/httpx/plugins/grpc.rb +2 -2
  38. data/lib/httpx/plugins/h2c.rb +23 -20
  39. data/lib/httpx/plugins/internal_telemetry.rb +48 -1
  40. data/lib/httpx/plugins/oauth.rb +1 -1
  41. data/lib/httpx/plugins/persistent.rb +16 -0
  42. data/lib/httpx/plugins/proxy/http.rb +19 -16
  43. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  44. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  45. data/lib/httpx/plugins/proxy.rb +96 -85
  46. data/lib/httpx/plugins/retries.rb +28 -10
  47. data/lib/httpx/plugins/ssrf_filter.rb +4 -1
  48. data/lib/httpx/plugins/stream.rb +42 -18
  49. data/lib/httpx/plugins/upgrade.rb +5 -10
  50. data/lib/httpx/plugins/webdav.rb +6 -0
  51. data/lib/httpx/plugins/xml.rb +76 -0
  52. data/lib/httpx/pool.rb +73 -244
  53. data/lib/httpx/request/body.rb +50 -55
  54. data/lib/httpx/request.rb +77 -14
  55. data/lib/httpx/resolver/https.rb +17 -20
  56. data/lib/httpx/resolver/multi.rb +34 -16
  57. data/lib/httpx/resolver/native.rb +140 -61
  58. data/lib/httpx/resolver/resolver.rb +64 -19
  59. data/lib/httpx/resolver/system.rb +32 -16
  60. data/lib/httpx/resolver.rb +21 -14
  61. data/lib/httpx/response/body.rb +12 -1
  62. data/lib/httpx/response.rb +16 -9
  63. data/lib/httpx/selector.rb +170 -91
  64. data/lib/httpx/session.rb +282 -139
  65. data/lib/httpx/timers.rb +17 -2
  66. data/lib/httpx/transcoder/body.rb +15 -29
  67. data/lib/httpx/transcoder/form.rb +2 -0
  68. data/lib/httpx/transcoder/gzip.rb +0 -3
  69. data/lib/httpx/transcoder/json.rb +16 -2
  70. data/lib/httpx/transcoder/multipart/encoder.rb +11 -2
  71. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  72. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  73. data/lib/httpx/transcoder.rb +0 -1
  74. data/lib/httpx/version.rb +1 -1
  75. data/lib/httpx.rb +20 -21
  76. data/sig/callbacks.rbs +2 -3
  77. data/sig/chainable.rbs +6 -2
  78. data/sig/connection/http1.rbs +2 -2
  79. data/sig/connection/http2.rbs +22 -18
  80. data/sig/connection.rbs +40 -9
  81. data/sig/errors.rbs +9 -3
  82. data/sig/httpx.rbs +3 -3
  83. data/sig/io/tcp.rbs +1 -1
  84. data/sig/io/unix.rbs +1 -1
  85. data/sig/loggable.rbs +4 -2
  86. data/sig/options.rbs +8 -13
  87. data/sig/plugins/aws_sigv4.rbs +8 -2
  88. data/sig/plugins/content_digest.rbs +51 -0
  89. data/sig/plugins/cookies/cookie.rbs +9 -0
  90. data/sig/plugins/follow_redirects.rbs +1 -1
  91. data/sig/plugins/grpc/call.rbs +4 -0
  92. data/sig/plugins/persistent.rbs +4 -1
  93. data/sig/plugins/proxy/http.rbs +3 -0
  94. data/sig/plugins/proxy/socks5.rbs +11 -3
  95. data/sig/plugins/proxy.rbs +18 -9
  96. data/sig/plugins/push_promise.rbs +6 -3
  97. data/sig/plugins/rate_limiter.rbs +2 -0
  98. data/sig/plugins/retries.rbs +1 -1
  99. data/sig/plugins/ssrf_filter.rbs +26 -0
  100. data/sig/plugins/stream.rbs +3 -0
  101. data/sig/plugins/webdav.rbs +23 -0
  102. data/sig/plugins/xml.rbs +37 -0
  103. data/sig/pool.rbs +27 -33
  104. data/sig/request/body.rbs +4 -10
  105. data/sig/request.rbs +14 -1
  106. data/sig/resolver/multi.rbs +26 -1
  107. data/sig/resolver/native.rbs +6 -3
  108. data/sig/resolver/resolver.rbs +22 -3
  109. data/sig/resolver.rbs +5 -1
  110. data/sig/response/body.rbs +2 -2
  111. data/sig/response/buffer.rbs +2 -2
  112. data/sig/response.rbs +9 -4
  113. data/sig/selector.rbs +31 -4
  114. data/sig/session.rbs +54 -20
  115. data/sig/timers.rbs +15 -4
  116. data/sig/transcoder/body.rbs +2 -4
  117. data/sig/transcoder/chunker.rbs +1 -1
  118. data/sig/transcoder/deflate.rbs +1 -0
  119. data/sig/transcoder/form.rbs +8 -0
  120. data/sig/transcoder/gzip.rbs +4 -1
  121. data/sig/transcoder/json.rbs +1 -1
  122. data/sig/transcoder/multipart.rbs +6 -4
  123. data/sig/transcoder/utils/body_reader.rbs +3 -3
  124. data/sig/transcoder/utils/deflater.rbs +2 -3
  125. metadata +32 -14
  126. data/lib/httpx/session2.rb +0 -23
  127. data/lib/httpx/transcoder/utils/inflater.rb +0 -19
  128. data/lib/httpx/transcoder/xml.rb +0 -52
  129. data/sig/transcoder/utils/inflater.rbs +0 -12
  130. data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/options.rb CHANGED
@@ -25,14 +25,14 @@ module HTTPX
25
25
  end
26
26
  rescue NotImplementedError
27
27
  [Socket::AF_INET]
28
- end
28
+ end.freeze
29
29
 
30
30
  DEFAULT_OPTIONS = {
31
31
  :max_requests => Float::INFINITY,
32
- :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
32
+ :debug => nil,
33
33
  :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
34
- :ssl => {},
35
- :http2_settings => { settings_enable_push: 0 },
34
+ :ssl => EMPTY_HASH,
35
+ :http2_settings => { settings_enable_push: 0 }.freeze,
36
36
  :fallback_protocol => "http/1.1",
37
37
  :supported_compression_formats => %w[gzip deflate],
38
38
  :decompress_response_body => true,
@@ -56,13 +56,15 @@ module HTTPX
56
56
  :response_class => Class.new(Response),
57
57
  :request_body_class => Class.new(Request::Body),
58
58
  :response_body_class => Class.new(Response::Body),
59
+ :pool_class => Class.new(Pool),
59
60
  :connection_class => Class.new(Connection),
60
61
  :options_class => Class.new(self),
61
62
  :transport => nil,
62
63
  :addresses => nil,
63
64
  :persistent => false,
64
65
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
65
- :resolver_options => { cache: true },
66
+ :resolver_options => { cache: true }.freeze,
67
+ :pool_options => EMPTY_HASH,
66
68
  :ip_families => ip_address_families,
67
69
  }.freeze
68
70
 
@@ -91,7 +93,7 @@ module HTTPX
91
93
  # :debug :: an object which log messages are written to (must respond to <tt><<</tt>)
92
94
  # :debug_level :: the log level of messages (can be 1, 2, or 3).
93
95
  # :ssl :: a hash of options which can be set as params of OpenSSL::SSL::SSLContext (see HTTPX::IO::SSL)
94
- # :http2_settings :: a hash of options to be passed to a HTTP2Next::Connection (ex: <tt>{ max_concurrent_streams: 2 }</tt>)
96
+ # :http2_settings :: a hash of options to be passed to a HTTP2::Connection (ex: <tt>{ max_concurrent_streams: 2 }</tt>)
95
97
  # :fallback_protocol :: version of HTTP protocol to use by default in the absence of protocol negotiation
96
98
  # like ALPN (defaults to <tt>"http/1.1"</tt>)
97
99
  # :supported_compression_formats :: list of compressions supported by the transcoder layer (defaults to <tt>%w[gzip deflate]</tt>).
@@ -110,6 +112,7 @@ module HTTPX
110
112
  # :request_body_class :: class used to instantiate a request body
111
113
  # :response_body_class :: class used to instantiate a response body
112
114
  # :connection_class :: class used to instantiate connections
115
+ # :pool_class :: class used to instantiate the session connection pool
113
116
  # :options_class :: class used to instantiate options
114
117
  # :transport :: type of transport to use (set to "unix" for UNIX sockets)
115
118
  # :addresses :: bucket of peer addresses (can be a list of IP addresses, a hash of domain to list of adddresses;
@@ -118,16 +121,13 @@ module HTTPX
118
121
  # :persistent :: whether to persist connections in between requests (defaults to <tt>true</tt>)
119
122
  # :resolver_class :: which resolver to use (defaults to <tt>:native</tt>, can also be <tt>:system<tt> for
120
123
  # using getaddrinfo or <tt>:https</tt> for DoH resolver, or a custom class)
121
- # :resolver_options :: hash of options passed to the resolver
124
+ # :resolver_options :: hash of options passed to the resolver. Accepted keys depend on the resolver type.
125
+ # :pool_options :: hash of options passed to the connection pool (See Pool#initialize).
122
126
  # :ip_families :: which socket families are supported (system-dependent)
123
127
  # :origin :: HTTP origin to set on requests with relative path (ex: "https://api.serv.com")
124
128
  # :base_path :: path to prefix given relative paths with (ex: "/v2")
125
129
  # :max_concurrent_requests :: max number of requests which can be set concurrently
126
130
  # :max_requests :: max number of requests which can be made on socket before it reconnects.
127
- # :params :: hash or array of key-values which will be encoded and set in the query string of request uris.
128
- # :form :: hash of array of key-values which will be form-or-multipart-encoded in requests body payload.
129
- # :json :: hash of array of key-values which will be JSON-encoded in requests body payload.
130
- # :xml :: Nokogiri XML nodes which will be encoded in requests body payload.
131
131
  #
132
132
  # This list of options are enhanced with each loaded plugin, see the plugin docs for details.
133
133
  def initialize(options = {})
@@ -216,19 +216,21 @@ module HTTPX
216
216
  end
217
217
 
218
218
  %i[
219
- params form json xml body ssl http2_settings
219
+ ssl http2_settings
220
220
  request_class response_class headers_class request_body_class
221
221
  response_body_class connection_class options_class
222
+ pool_class pool_options
222
223
  io fallback_protocol debug debug_level resolver_class resolver_options
223
224
  compress_request_body decompress_response_body
224
225
  persistent
225
226
  ].each do |method_name|
226
227
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
228
+ # sets +v+ as the value of #{method_name}
227
229
  def option_#{method_name}(v); v; end # def option_smth(v); v; end
228
230
  OUT
229
231
  end
230
232
 
231
- REQUEST_BODY_IVARS = %i[@headers @params @form @xml @json @body].freeze
233
+ REQUEST_BODY_IVARS = %i[@headers].freeze
232
234
 
233
235
  def ==(other)
234
236
  super || options_equals?(other)
@@ -249,14 +251,6 @@ module HTTPX
249
251
  end
250
252
  end
251
253
 
252
- OTHER_LOOKUP = ->(obj, k, ivar_map) {
253
- case obj
254
- when Hash
255
- obj[ivar_map[k]]
256
- else
257
- obj.instance_variable_get(k)
258
- end
259
- }
260
254
  def merge(other)
261
255
  ivar_map = nil
262
256
  other_ivars = case other
@@ -269,12 +263,12 @@ module HTTPX
269
263
 
270
264
  return self if other_ivars.empty?
271
265
 
272
- return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == OTHER_LOOKUP[other, ivar, ivar_map] }
266
+ return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == access_option(other, ivar, ivar_map) }
273
267
 
274
268
  opts = dup
275
269
 
276
270
  other_ivars.each do |ivar|
277
- v = OTHER_LOOKUP[other, ivar, ivar_map]
271
+ v = access_option(other, ivar, ivar_map)
278
272
 
279
273
  unless v
280
274
  opts.instance_variable_set(ivar, v)
@@ -325,6 +319,10 @@ module HTTPX
325
319
  @response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
326
320
  @response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
327
321
  end
322
+ if defined?(pl::PoolMethods)
323
+ @pool_class = @pool_class.dup
324
+ @pool_class.__send__(:include, pl::PoolMethods)
325
+ end
328
326
  if defined?(pl::ConnectionMethods)
329
327
  @connection_class = @connection_class.dup
330
328
  @connection_class.__send__(:include, pl::ConnectionMethods)
@@ -349,5 +347,14 @@ module HTTPX
349
347
  instance_variable_set(:"@#{k}", value)
350
348
  end
351
349
  end
350
+
351
+ def access_option(obj, k, ivar_map)
352
+ case obj
353
+ when Hash
354
+ obj[ivar_map[k]]
355
+ else
356
+ obj.instance_variable_get(k)
357
+ end
358
+ end
352
359
  end
353
360
  end
@@ -72,6 +72,9 @@ module HTTPX
72
72
  end
73
73
  end
74
74
 
75
+ # adds support for the following options:
76
+ #
77
+ # :aws_profile :: AWS account profile to retrieve credentials from.
75
78
  module OptionsMethods
76
79
  def option_aws_profile(value)
77
80
  String(value)
@@ -12,6 +12,7 @@ module HTTPX
12
12
  module AWSSigV4
13
13
  Credentials = Struct.new(:username, :password, :security_token)
14
14
 
15
+ # Signs requests using the AWS sigv4 signing.
15
16
  class Signer
16
17
  def initialize(
17
18
  service:,
@@ -88,7 +89,7 @@ module HTTPX
88
89
  sts = "#{algo_line}" \
89
90
  "\n#{datetime}" \
90
91
  "\n#{credential_scope}" \
91
- "\n#{hexdigest(creq)}"
92
+ "\n#{OpenSSL::Digest.new(@algorithm).hexdigest(creq)}"
92
93
 
93
94
  # signature
94
95
  k_date = hmac("#{upper_provider_prefix}#{@credentials.password}", date)
@@ -109,22 +110,38 @@ module HTTPX
109
110
  private
110
111
 
111
112
  def hexdigest(value)
112
- if value.respond_to?(:to_path)
113
- # files, pathnames
114
- OpenSSL::Digest.new(@algorithm).file(value.to_path).hexdigest
115
- elsif value.respond_to?(:each)
116
- digest = OpenSSL::Digest.new(@algorithm)
117
-
118
- mb_buffer = value.each.with_object("".b) do |chunk, buffer|
119
- buffer << chunk
120
- break if buffer.bytesize >= 1024 * 1024
121
- end
113
+ digest = OpenSSL::Digest.new(@algorithm)
122
114
 
123
- digest.update(mb_buffer)
124
- value.rewind
125
- digest.hexdigest
115
+ if value.respond_to?(:read)
116
+ if value.respond_to?(:to_path)
117
+ # files, pathnames
118
+ digest.file(value.to_path).hexdigest
119
+ else
120
+ # gzipped request bodies
121
+ raise Error, "request body must be rewindable" unless value.respond_to?(:rewind)
122
+
123
+ buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
124
+ begin
125
+ IO.copy_stream(value, buffer)
126
+ buffer.flush
127
+
128
+ digest.file(buffer.to_path).hexdigest
129
+ ensure
130
+ value.rewind
131
+ buffer.close
132
+ buffer.unlink
133
+ end
134
+ end
126
135
  else
127
- OpenSSL::Digest.new(@algorithm).hexdigest(value)
136
+ # error on endless generators
137
+ raise Error, "hexdigest for endless enumerators is not supported" if value.unbounded_body?
138
+
139
+ mb_buffer = value.each.with_object("".b) do |chunk, b|
140
+ b << chunk
141
+ break if b.bytesize >= 1024 * 1024
142
+ end
143
+
144
+ digest.hexdigest(mb_buffer)
128
145
  end
129
146
  end
130
147
 
@@ -141,7 +158,6 @@ module HTTPX
141
158
  def load_dependencies(*)
142
159
  require "set"
143
160
  require "digest/sha2"
144
- require "openssl"
145
161
  end
146
162
 
147
163
  def configure(klass)
@@ -149,6 +165,9 @@ module HTTPX
149
165
  end
150
166
  end
151
167
 
168
+ # adds support for the following options:
169
+ #
170
+ # :sigv4_signer :: instance of HTTPX::Plugins::AWSSigV4 used to sign requests.
152
171
  module OptionsMethods
153
172
  def option_sigv4_signer(value)
154
173
  value.is_a?(Signer) ? value : Signer.new(value)
@@ -160,7 +179,7 @@ module HTTPX
160
179
  with(sigv4_signer: Signer.new(**options))
161
180
  end
162
181
 
163
- def build_request(*, _)
182
+ def build_request(*)
164
183
  request = super
165
184
 
166
185
  return request if request.headers.key?("authorization")
@@ -25,18 +25,23 @@ module HTTPX
25
25
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
26
26
  def on_#{meth}(&blk) # def on_connection_opened(&blk)
27
27
  on(:#{meth}, &blk) # on(:connection_opened, &blk)
28
+ self # self
28
29
  end # end
29
30
  MOD
30
31
  end
31
32
 
32
33
  private
33
34
 
34
- def init_connection(uri, options)
35
- connection = super
35
+ def do_init_connection(connection, selector)
36
+ super
36
37
  connection.on(:open) do
38
+ next unless connection.current_session == self
39
+
37
40
  emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
38
41
  end
39
42
  connection.on(:close) do
43
+ next unless connection.current_session == self
44
+
40
45
  emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
41
46
  end
42
47
 
@@ -84,6 +89,12 @@ module HTTPX
84
89
  rescue CallbackError => e
85
90
  raise e.cause
86
91
  end
92
+
93
+ def close(*)
94
+ super
95
+ rescue CallbackError => e
96
+ raise e.cause
97
+ end
87
98
  end
88
99
  end
89
100
  register_plugin :callbacks, Callbacks
@@ -32,15 +32,11 @@ module HTTPX
32
32
  @circuit_store = CircuitStore.new(@options)
33
33
  end
34
34
 
35
- def initialize_dup(orig)
36
- super
37
- @circuit_store = orig.instance_variable_get(:@circuit_store).dup
38
- end
39
-
40
35
  %i[circuit_open].each do |meth|
41
36
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
42
37
  def on_#{meth}(&blk) # def on_circuit_open(&blk)
43
38
  on(:#{meth}, &blk) # on(:circuit_open, &blk)
39
+ self # self
44
40
  end # end
45
41
  MOD
46
42
  end
@@ -97,6 +93,16 @@ module HTTPX
97
93
  end
98
94
  end
99
95
 
96
+ # adds support for the following options:
97
+ #
98
+ # :circuit_breaker_max_attempts :: the number of attempts the circuit allows, before it is opened (defaults to <tt>3</tt>).
99
+ # :circuit_breaker_reset_attempts_in :: the time a circuit stays open at most, before it resets (defaults to <tt>60</tt>).
100
+ # :circuit_breaker_break_on :: callable defining an alternative rule for a response to break
101
+ # (i.e. <tt>->(res) { res.status == 429 } </tt>)
102
+ # :circuit_breaker_break_in :: the time that must elapse before an open circuit can transit to the half-open state
103
+ # (defaults to <tt><60</tt>).
104
+ # :circuit_breaker_half_open_drip_rate :: the rate of requests a circuit allows to be performed when in an half-open state
105
+ # (defaults to <tt>1</tt>).
100
106
  module OptionsMethods
101
107
  def option_circuit_breaker_max_attempts(value)
102
108
  attempts = Integer(value)
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds `Content-Digest` headers to requests
7
+ # and can validate these headers on responses
8
+ #
9
+ # https://datatracker.ietf.org/doc/html/rfc9530
10
+ #
11
+ module ContentDigest
12
+ class Error < HTTPX::Error; end
13
+
14
+ # Error raised on response "content-digest" header validation.
15
+ class ValidationError < Error
16
+ attr_reader :response
17
+
18
+ def initialize(message, response)
19
+ super(message)
20
+ @response = response
21
+ end
22
+ end
23
+
24
+ class MissingContentDigestError < ValidationError; end
25
+ class InvalidContentDigestError < ValidationError; end
26
+
27
+ SUPPORTED_ALGORITHMS = {
28
+ "sha-256" => OpenSSL::Digest::SHA256,
29
+ "sha-512" => OpenSSL::Digest::SHA512,
30
+ }.freeze
31
+
32
+ class << self
33
+ def extra_options(options)
34
+ options.merge(encode_content_digest: true, validate_content_digest: false, content_digest_algorithm: "sha-256")
35
+ end
36
+ end
37
+
38
+ # add support for the following options:
39
+ #
40
+ # :content_digest_algorithm :: the digest algorithm to use. Currently supports `sha-256` and `sha-512`. (defaults to `sha-256`)
41
+ # :encode_content_digest :: whether a <tt>Content-Digest</tt> header should be computed for the request;
42
+ # can also be a callable object (i.e. <tt>->(req) { ... }</tt>, defaults to <tt>true</tt>)
43
+ # :validate_content_digest :: whether a <tt>Content-Digest</tt> header in the response should be validated;
44
+ # can also be a callable object (i.e. <tt>->(res) { ... }</tt>, defaults to <tt>false</tt>)
45
+ module OptionsMethods
46
+ def option_content_digest_algorithm(value)
47
+ raise TypeError, ":content_digest_algorithm must be one of 'sha-256', 'sha-512'" unless SUPPORTED_ALGORITHMS.key?(value)
48
+
49
+ value
50
+ end
51
+
52
+ def option_encode_content_digest(value)
53
+ value
54
+ end
55
+
56
+ def option_validate_content_digest(value)
57
+ value
58
+ end
59
+ end
60
+
61
+ module ResponseBodyMethods
62
+ attr_reader :content_digest_buffer
63
+
64
+ def initialize(response, options)
65
+ super
66
+
67
+ return unless response.headers.key?("content-digest")
68
+
69
+ should_validate = options.validate_content_digest
70
+ should_validate = should_validate.call(response) if should_validate.respond_to?(:call)
71
+
72
+ return unless should_validate
73
+
74
+ @content_digest_buffer = Response::Buffer.new(
75
+ threshold_size: @options.body_threshold_size,
76
+ bytesize: @length,
77
+ encoding: @encoding
78
+ )
79
+ end
80
+
81
+ def write(chunk)
82
+ @content_digest_buffer.write(chunk) if @content_digest_buffer
83
+ super
84
+ end
85
+
86
+ def close
87
+ if @content_digest_buffer
88
+ @content_digest_buffer.close
89
+ @content_digest_buffer = nil
90
+ end
91
+ super
92
+ end
93
+ end
94
+
95
+ module InstanceMethods
96
+ def build_request(*)
97
+ request = super
98
+
99
+ return request if request.empty?
100
+
101
+ return request if request.headers.key?("content-digest")
102
+
103
+ perform_encoding = @options.encode_content_digest
104
+ perform_encoding = perform_encoding.call(request) if perform_encoding.respond_to?(:call)
105
+
106
+ return request unless perform_encoding
107
+
108
+ digest = base64digest(request.body)
109
+ request.headers.add("content-digest", "#{@options.content_digest_algorithm}=:#{digest}:")
110
+
111
+ request
112
+ end
113
+
114
+ private
115
+
116
+ def fetch_response(request, _, _)
117
+ response = super
118
+ return response unless response.is_a?(Response)
119
+
120
+ perform_validation = @options.validate_content_digest
121
+ perform_validation = perform_validation.call(response) if perform_validation.respond_to?(:call)
122
+
123
+ validate_content_digest(response) if perform_validation
124
+
125
+ response
126
+ rescue ValidationError => e
127
+ ErrorResponse.new(request, e)
128
+ end
129
+
130
+ def validate_content_digest(response)
131
+ content_digest_header = response.headers["content-digest"]
132
+
133
+ raise MissingContentDigestError.new("response is missing a `content-digest` header", response) unless content_digest_header
134
+
135
+ digests = extract_content_digests(content_digest_header)
136
+
137
+ included_algorithms = SUPPORTED_ALGORITHMS.keys & digests.keys
138
+
139
+ raise MissingContentDigestError.new("unsupported algorithms: #{digests.keys.join(", ")}", response) if included_algorithms.empty?
140
+
141
+ content_buffer = response.body.content_digest_buffer
142
+
143
+ included_algorithms.each do |algorithm|
144
+ digest = SUPPORTED_ALGORITHMS.fetch(algorithm).new
145
+ digest_received = digests[algorithm]
146
+ digest_computed =
147
+ if content_buffer.respond_to?(:to_path)
148
+ content_buffer.flush
149
+ digest.file(content_buffer.to_path).base64digest
150
+ else
151
+ digest.base64digest(content_buffer.to_s)
152
+ end
153
+
154
+ raise InvalidContentDigestError.new("#{algorithm} digest does not match content",
155
+ response) unless digest_received == digest_computed
156
+ end
157
+ end
158
+
159
+ def extract_content_digests(header)
160
+ header.split(",").to_h do |entry|
161
+ algorithm, digest = entry.split("=", 2)
162
+ raise Error, "#{entry} is an invalid digest format" unless algorithm && digest
163
+
164
+ [algorithm, digest.byteslice(1..-2)]
165
+ end
166
+ end
167
+
168
+ def base64digest(body)
169
+ digest = SUPPORTED_ALGORITHMS.fetch(@options.content_digest_algorithm).new
170
+
171
+ if body.respond_to?(:read)
172
+ if body.respond_to?(:to_path)
173
+ digest.file(body.to_path).base64digest
174
+ else
175
+ raise ContentDigestError, "request body must be rewindable" unless body.respond_to?(:rewind)
176
+
177
+ buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
178
+ begin
179
+ IO.copy_stream(body, buffer)
180
+ buffer.flush
181
+
182
+ digest.file(buffer.to_path).base64digest
183
+ ensure
184
+ body.rewind
185
+ buffer.close
186
+ buffer.unlink
187
+ end
188
+ end
189
+ else
190
+ raise ContentDigestError, "base64digest for endless enumerators is not supported" if body.unbounded_body?
191
+
192
+ buffer = "".b
193
+ body.each { |chunk| buffer << chunk }
194
+
195
+ digest.base64digest(buffer)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ register_plugin :content_digest, ContentDigest
201
+ end
202
+ end
@@ -40,6 +40,12 @@ module HTTPX
40
40
  end
41
41
  end
42
42
 
43
+ def build_request(*)
44
+ request = super
45
+ request.headers.set_cookie(request.options.cookies[request.uri])
46
+ request
47
+ end
48
+
43
49
  private
44
50
 
45
51
  def on_response(_request, response)
@@ -52,12 +58,6 @@ module HTTPX
52
58
 
53
59
  super
54
60
  end
55
-
56
- def build_request(*, _)
57
- request = super
58
- request.headers.set_cookie(request.options.cookies[request.uri])
59
- request
60
- end
61
61
  end
62
62
 
63
63
  module HeadersMethods
@@ -70,6 +70,9 @@ module HTTPX
70
70
  end
71
71
  end
72
72
 
73
+ # adds support for the following options:
74
+ #
75
+ # :cookies :: cookie jar for the session (can be a Hash, an Array, an instance of HTTPX::Plugins::Cookies::CookieJar)
73
76
  module OptionsMethods
74
77
  def option_headers(*)
75
78
  value = super
@@ -20,6 +20,9 @@ module HTTPX
20
20
  end
21
21
  end
22
22
 
23
+ # adds support for the following options:
24
+ #
25
+ # :digest :: instance of HTTPX::Plugins::Authentication::Digest, used to authenticate requests in the session.
23
26
  module OptionsMethods
24
27
  def option_digest(value)
25
28
  raise TypeError, ":digest must be a #{Authentication::Digest}" unless value.is_a?(Authentication::Digest)
@@ -20,6 +20,11 @@ module HTTPX
20
20
  end
21
21
  end
22
22
 
23
+ # adds support for the following options:
24
+ #
25
+ # :expect_timeout :: time (in seconds) to wait for a 100-expect response,
26
+ # before retrying without the Expect header (defaults to <tt>2</tt>).
27
+ # :expect_threshold_size :: min threshold (in bytes) of the request payload to enable the 100-continue negotiation on.
23
28
  module OptionsMethods
24
29
  def option_expect_timeout(value)
25
30
  seconds = Float(value)
@@ -79,7 +84,7 @@ module HTTPX
79
84
 
80
85
  return if expect_timeout.nil? || expect_timeout.infinite?
81
86
 
82
- set_request_timeout(request, expect_timeout, :expect, %i[body response]) do
87
+ set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
83
88
  # expect timeout expired
84
89
  if request.state == :expect && !request.expects?
85
90
  Expect.no_expect_store << request.origin
@@ -91,15 +96,16 @@ module HTTPX
91
96
  end
92
97
 
93
98
  module InstanceMethods
94
- def fetch_response(request, connections, options)
95
- response = @responses.delete(request)
99
+ def fetch_response(request, selector, options)
100
+ response = super
101
+
96
102
  return unless response
97
103
 
98
104
  if response.is_a?(Response) && response.status == 417 && request.headers.key?("expect")
99
105
  response.close
100
106
  request.headers.delete("expect")
101
107
  request.transition(:idle)
102
- send_request(request, connections, options)
108
+ send_request(request, selector, options)
103
109
  return
104
110
  end
105
111