httpx 1.6.3 → 1.7.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +2 -2
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/altsvc.rb +3 -1
  8. data/lib/httpx/connection/http1.rb +5 -6
  9. data/lib/httpx/connection/http2.rb +2 -0
  10. data/lib/httpx/connection.rb +3 -8
  11. data/lib/httpx/domain_name.rb +1 -1
  12. data/lib/httpx/headers.rb +2 -2
  13. data/lib/httpx/loggable.rb +2 -0
  14. data/lib/httpx/options.rb +40 -17
  15. data/lib/httpx/plugins/auth/digest.rb +44 -4
  16. data/lib/httpx/plugins/auth.rb +87 -4
  17. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  18. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  19. data/lib/httpx/plugins/digest_auth.rb +4 -5
  20. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  21. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  22. data/lib/httpx/plugins/grpc.rb +2 -2
  23. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  24. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  25. data/lib/httpx/plugins/oauth.rb +162 -56
  26. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  27. data/lib/httpx/plugins/response_cache.rb +3 -7
  28. data/lib/httpx/plugins/retries.rb +55 -16
  29. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  30. data/lib/httpx/plugins/stream.rb +59 -8
  31. data/lib/httpx/plugins/stream_bidi.rb +73 -17
  32. data/lib/httpx/pool.rb +12 -2
  33. data/lib/httpx/request.rb +10 -1
  34. data/lib/httpx/resolver/https.rb +67 -17
  35. data/lib/httpx/resolver/multi.rb +4 -0
  36. data/lib/httpx/resolver/native.rb +26 -4
  37. data/lib/httpx/resolver/resolver.rb +2 -2
  38. data/lib/httpx/resolver.rb +97 -29
  39. data/lib/httpx/response/body.rb +2 -0
  40. data/lib/httpx/response.rb +22 -6
  41. data/lib/httpx/selector.rb +9 -0
  42. data/lib/httpx/session.rb +6 -6
  43. data/lib/httpx/transcoder/body.rb +1 -1
  44. data/lib/httpx/transcoder/json.rb +1 -1
  45. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  46. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  47. data/lib/httpx/transcoder/multipart.rb +16 -8
  48. data/lib/httpx/transcoder.rb +4 -6
  49. data/lib/httpx/version.rb +1 -1
  50. data/sig/altsvc.rbs +3 -0
  51. data/sig/chainable.rbs +3 -3
  52. data/sig/connection.rbs +1 -3
  53. data/sig/options.rbs +1 -1
  54. data/sig/plugins/auth/digest.rbs +6 -0
  55. data/sig/plugins/auth.rbs +28 -4
  56. data/sig/plugins/basic_auth.rbs +3 -3
  57. data/sig/plugins/digest_auth.rbs +2 -4
  58. data/sig/plugins/fiber_concurrency.rbs +6 -0
  59. data/sig/plugins/ntlm_auth.rbs +2 -2
  60. data/sig/plugins/oauth.rbs +46 -15
  61. data/sig/plugins/rate_limiter.rbs +1 -1
  62. data/sig/plugins/response_cache/file_store.rbs +2 -0
  63. data/sig/plugins/response_cache.rbs +4 -0
  64. data/sig/plugins/retries.rbs +8 -2
  65. data/sig/plugins/stream.rbs +13 -3
  66. data/sig/plugins/stream_bidi.rbs +2 -2
  67. data/sig/pool.rbs +1 -1
  68. data/sig/resolver/https.rbs +5 -0
  69. data/sig/resolver/multi.rbs +2 -0
  70. data/sig/resolver/native.rbs +2 -0
  71. data/sig/resolver.rbs +12 -3
  72. data/sig/response.rbs +3 -0
  73. data/sig/session.rbs +3 -5
  74. data/sig/transcoder/multipart.rbs +4 -2
  75. data/sig/transcoder.rbs +5 -1
  76. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f49b29ea3703f6f40abe3cd82d455235b0a0b50a694bd8fa55839ac471d32bbb
4
- data.tar.gz: ce933bb3c35d9434f810d4fc83acb244a1ad0db187123d5f01dcbbbd83622a47
3
+ metadata.gz: 2af63a63fe08211db58764570618b2e7c234d3473a28fc1ad6f5a31c3d0fb13d
4
+ data.tar.gz: 6b2671b85ac69e4817b8b764dfdabf638dacf94cfdcab6c44627ad64a57bcb50
5
5
  SHA512:
6
- metadata.gz: 258cb32129840347a1a37633fb6273133187c98636b63ffcc0b8064f39e3b5642d693ee5da5d7776529625fbe4b68b5193b405c2e0658b2c9b1193b49e316bdc
7
- data.tar.gz: 387397ab1954b6abf6a8a54d8764fb25e786fb1bd293401e8afd57f823464f67210527fee0b9e94b34cc3d2931143a5d94598fb6241f5728df77a8f1e045a5bf
6
+ metadata.gz: 50a2d2d3c0cb27f3bf84cc34553b06bd9fadf46ad789f9cfa08091cc458ac07751cb5593e01d242d88e444f10ac2b3014ea3e4e56f7c616ebc860517d08ef90d
7
+ data.tar.gz: 6b15f21262e85639c6d32b9851926d7e717b6f9bcbc226b9af3872d67461799fd24847a2d563dfbad1967d5349d371c18a1a6e1a28c721c5f33387f2fc1446a7
@@ -21,7 +21,7 @@ stub_http_request(:get, "https://www.google.com").and_return(status: 200, body:
21
21
 
22
22
  ```
23
23
 
24
- Read more about it in the [webmock integration documentation](https://os85.gitlab.io/httpx/wiki/Webmock-Adapter).
24
+ Read more about it in the [webmock integration documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Webmock-Adapter).
25
25
 
26
26
  ### Datadog Adapter
27
27
 
@@ -40,7 +40,7 @@ A trace will be emitted for every request, so this should be an interesting visu
40
40
 
41
41
  Customization options and traces are similar to what [the net-http adapter provides](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#nethttp).
42
42
 
43
- Read more about it in the [datadog integration documentation](https://os85.gitlab.io/httpx/wiki/Datadog-Adapter).
43
+ Read more about it in the [datadog integration documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Datadog-Adapter).
44
44
 
45
45
  ## Improvements
46
46
 
@@ -52,7 +52,7 @@ Read more about it in the [datadog integration documentation](https://os85.gitla
52
52
  HTTPX.plugin(:multipart).post(uri, form: {file: File.new("path/to/file")})
53
53
  ```
54
54
 
55
- Read more about it in the [multipart plugin documentation](https://os85.gitlab.io/httpx/wiki/Multipart-Uploads), including also about why this was made.
55
+ Read more about it in the [multipart plugin documentation](https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads), including also about why this was made.
56
56
 
57
57
  ### Expect Plugin
58
58
 
@@ -7,8 +7,8 @@
7
7
  ## Improvements
8
8
 
9
9
  * `system` resolver now works in a non-blocking manner, initiating the dns query in a separate thread and waiting on the pipe after that (it was blocking the main thread during resolution before).
10
- * reduce allocation to a sinfle shared option object when headers are passed as a session-level option, like `HTTPX.with(headers: geaders).get(...)`
11
- * privilege using `String#replace` in buffer operations (instead of "clean-then-append").
10
+ * reduce allocation to a single shared option object when headers are passed as a session-level option, like `HTTPX.with(headers: headers).get(...)`
11
+ * favour using `String#replace` in buffer operations (instead of "clean-then-append").
12
12
  * using `Array#unshift` instead of `Array#concat` in order to ensure that request ordering is respected in the face of an in-between error which requires reconnect-and-resend.
13
13
  * replaced more internal callback indirection with plain method calls.
14
14
 
@@ -0,0 +1,149 @@
1
+ # 1.7.0
2
+
3
+ ## Features
4
+
5
+ ### All AUTH plugin improvements!!
6
+
7
+ #### `:auth`
8
+
9
+ The `:auth` plugin can now be used with a dynamic callable object (methods, procs...) to generate the token.
10
+
11
+ ```ruby
12
+ # static token, pre 1.7.0
13
+ HTTPX.plugin(:auth).authorization("API-TOKEN")
14
+ # dynamically generate token!
15
+ HTTPX.plugin(:auth).authorization { generate_new_ephemeral_token }
16
+ ```
17
+
18
+ The `.authorization` method is now syntactic sugar for a new option, `:auth_header_value`, which can be used directly, alongside a `:auth_header_type`:
19
+
20
+ ```ruby
21
+ HTTPX.plugin(:auth).authorization("API-TOKEN")
22
+ HTTPX.plugin(:auth).authorization { generate_new_ephemeral_token }
23
+ HTTPX.plugin(:auth).authorization("Bearer API-TOKEN")
24
+ # same as
25
+ HTTPX.plugin(:auth, auth_header_value: "API-TOKEN")
26
+ HTTPX.plugin(:auth, auth_header_value: -> { generate_new_ephemeral_token })
27
+ HTTPX.plugin(:auth, auth_header_type: "Bearer", auth_header_value: "API-TOKEN")
28
+ ```
29
+
30
+ A new option `:generate_auth_value_on_retry` (which can be passed a callable receiving a response object) is now available; when used alongside the `:retries` plugin, it'll use the callable passed to the `.authorization` method to generate a new token before retrying the request:
31
+
32
+ ```ruby
33
+ authed = HTTPX.plugin(:retries).plugin(:auth, generate_auth_value_on_retry: ->(res) {
34
+ res.status == 401
35
+ }).authorization { generate_new_ephemeral_token }
36
+ authed.get("https://example.com")
37
+ ```
38
+
39
+ Read more about it in the [auth plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Auth).
40
+
41
+ #### `:oauth`
42
+
43
+ The `:oauth` plugin implementation was revamped to make use of the `:auth` plugin new functionality, in order to make managing an oauth session more seamless.
44
+
45
+ Take the following example:
46
+
47
+ ```ruby
48
+ session = HTTPX.plugin(:oauth).with_oauth_options(
49
+ issuer: server.origin,
50
+ client_id: "CLIENT_ID",
51
+ client_secret: "SECRET",
52
+ )
53
+ session.get("https://example.com") #=> will load server metadata, request an access token, and perform the request with the access token.
54
+ # 2 hours later...
55
+ session.get("https://example.com")
56
+ # it'll reuse the same acces token, and if the request fails with 401, it'll request a new
57
+ # access token using the refresh token grant (when supported by the token issuer), and
58
+ # reperform the original request with the new access token.
59
+ ```
60
+
61
+ A new option, `:oauth_options`, is now available. The same parameters previously supported by the `:oauth_session` options are supported.
62
+
63
+ The following components are therefore deprecated and scheduled for removal in a future major version:
64
+
65
+ * `:oauth_session` option
66
+ * `.oauth_auth` session method
67
+ * `.with_access_token` session method
68
+
69
+ #### `:bearer_auth`, `:digest_auth`; `:ntlm_auth`
70
+
71
+ The `:auth` plugin is now the foundation of each of these plugins, which haven't suffered major API changes.
72
+
73
+ Read more about it in the [auth plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/OAuth).
74
+
75
+ ### `:retries` plugin: `:retry_after` backoff algorithms
76
+
77
+ The `:retries` plugins supports two new possible values for the `:retry_after` option: `:exponential_backoff` and `:polynomial_backoff`. They'll implement the respective calculation per each retry of a given request.
78
+
79
+ ```ruby
80
+ # will wait 1, 2, 4, 8, 16 seconds... depending of how many retries it can wait for
81
+ session = HTTPX.plugin(:retries, retry_after: :exponential_backoff)
82
+ ```
83
+
84
+ Read more about it in the [retries plugin wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Retries).
85
+
86
+ ### Ractor compatibility
87
+
88
+ `httpx` can be used within a ractor:
89
+
90
+ ```ruby
91
+ # ruby 4.0 syntax
92
+ response = Ractor.new(uri) do |uri|
93
+ HTTPX.get(uri)
94
+ end.value
95
+ ```
96
+
97
+ Bear in mind that, if you're connection via HTTPS, you'll need make sure you're using version 4.0 or higher of the `openssl` gem.
98
+
99
+ The test suite isn't exhaustive for ractors yet, but most plugins should also be ractor-compatible. If they don't work, that's a bug, and you're recommended to report it.
100
+
101
+ ## Improvements
102
+
103
+ * When encoding the `:json` param to send it as `application/json` payload, (example: `HTTPX.post("https://example.com", json: { foo: "bar })`), and the method uses the `json` standard library, it'll use `JSON.generate` (instead of `JSON.dump`) to encode the JSON payload. The reason is that, unlike `JSON.dump`, it doesn't rely on access to a global mutable hash, and is therefore ractor-safe.
104
+ * `:stream` plugin: the stream response class (the object that is returned in request calls is a stream response) can be extended now. You can add a `StreamResponseMethods` method to your plugin. Read more about it in the documentation.
105
+ * The resolver name cache (used by the native and https resolvers) was remade into a LRU cache, and will therefore not keep on growing when `httpx` is used to connect to a huge number of hostnames in a process.
106
+ * the native and https DNS resolvers will ignore answers with SERVFAIL code while there are retries left (some resolvers use such error code for rate limiting).
107
+ * `:timeout` option values are now validated, and an error is raised when passing an unrecognized timeout option (which is a good layer of protection for typos).
108
+ * pool: try passing the scheduler to a thread waiting on a connection, to avoid the current case where a connection may be checked-in-then-immediately-out-after when doing multiple requests in a loop, never giving a chance to others and potentially making the pool time out.
109
+ * headers deep-freeze and dup.
110
+
111
+ ## Bugfixes
112
+
113
+ * recover and close connection when an `IOError` is raised while waiting for IO readiness (could cause busy loops during HTTP/2 termination handshake).
114
+ * `:stream_bidi` plugin: improve thread-safety of buffer operations when the session is used from multiple threads.
115
+ * `:stream_bidi` plugin: added missing methods to signal in order to comply with the Selectable API (it was reported as raising `NoMethodError` under certain conditions).
116
+ * `:stream_bidi` plugin: can support non-bidirectional stream requests using the same session.
117
+ * `:stream` plugin: is now compatible with fiber scheduler engines (via the `:fiber_concurrency` plugin).
118
+ * `:stream` plugin: make sure that stream long-running requests do not share the same connection as regular threads.
119
+ * `:digest_auth` plugin: can now support qop values wrapped inside parentheses in the `www-authenticate` header (i.e. `qop="('auth',)"`).
120
+ * https resolver: handle 3XX redirect responses in HTTP DNS queries.
121
+ * https resolver: do not close HTTP connections whhich are shared across AAAA and A resolution paths when its in use by one of them.
122
+ * fix access to private method from `http-2` which was made public in more recent versions, but not in older still-supported versions.
123
+ * fixed resolver log message using a "connection" label.
124
+ * `HTTPX::Response.copy_to` will explicitly close the response at the end; given that the body file can be moved as a result, there is no guarantee that the response is still usable, so might as well just close it altogether.
125
+ * selector: avoid skipping persistent connections in the selector to deactivate due to iterate-and-modify.
126
+
127
+ ## Breaking Changes
128
+
129
+ ### `:digest_auth` error
130
+
131
+ The main error class for the `:digest_auth` plugin has been moved to a different location. If you were rescuing the `HTTPX::Plugins::DigestAuth::DigestError` error, you should now point to the `HTTPX::Authentication::Digest::Error`.
132
+
133
+ ### `:stream` plugin: `build_request` should receive `stream: true` for stream requests
134
+
135
+ In case you're building request objects before passing them to the session, you're now forced to create them with the `:stream` option on:
136
+
137
+ ```ruby
138
+ session = HTTPX.plugin(:stream)
139
+
140
+ # before
141
+ req = session.build_request("GET", "https://example.com/stream")
142
+ session.request(req, stream: true)
143
+
144
+ # after
145
+ req = session.build_request("GET", "https://example.com/stream", stream: true)
146
+ session.request(req)
147
+ ```
148
+
149
+ Previous code may still work in a few cases, but it is not guaranteed to work on all cases.
@@ -80,7 +80,7 @@ module Datadog::Tracing
80
80
  else
81
81
  span.set_tag(TAG_STATUS_CODE, response.status.to_s)
82
82
 
83
- span.set_error(::HTTPX::HTTPError.new(response)) if response.status >= 400 && response.status <= 599
83
+ span.set_error(::HTTPX::HTTPError.new(response)) if response.status.between?(400, 599)
84
84
 
85
85
  span.set_tags(
86
86
  Datadog.configuration.tracing.header_tags.response_tags(response.headers.to_h)
@@ -9,7 +9,7 @@ module Faraday
9
9
  class HTTPX < Faraday::Adapter
10
10
  def initialize(app = nil, opts = {}, &block)
11
11
  @connection = @bind = nil
12
- super(app, opts, &block)
12
+ super
13
13
  end
14
14
 
15
15
  module RequestMixin
data/lib/httpx/altsvc.rb CHANGED
@@ -8,6 +8,8 @@ module HTTPX
8
8
  module ConnectionMixin
9
9
  using URIExtensions
10
10
 
11
+ H2_ALTSVC_SCHEMES = %w[https h2].freeze
12
+
11
13
  def send(request)
12
14
  request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
13
15
 
@@ -46,7 +48,7 @@ module HTTPX
46
48
  uri.origin == other_uri.origin || begin
47
49
  case uri.scheme
48
50
  when "h2"
49
- (other_uri.scheme == "https" || other_uri.scheme == "h2") &&
51
+ H2_ALTSVC_SCHEMES.include?(other_uri.scheme) &&
50
52
  uri.host == other_uri.host &&
51
53
  uri.port == other_uri.port
52
54
  else
@@ -10,6 +10,11 @@ module HTTPX
10
10
  MAX_REQUESTS = 200
11
11
  CRLF = "\r\n"
12
12
 
13
+ UPCASED = {
14
+ "www-authenticate" => "WWW-Authenticate",
15
+ "http2-settings" => "HTTP2-Settings",
16
+ "content-md5" => "Content-MD5",
17
+ }.freeze
13
18
  attr_reader :pending, :requests
14
19
 
15
20
  attr_accessor :max_concurrent_requests
@@ -386,12 +391,6 @@ module HTTPX
386
391
  end
387
392
  end
388
393
 
389
- UPCASED = {
390
- "www-authenticate" => "WWW-Authenticate",
391
- "http2-settings" => "HTTP2-Settings",
392
- "content-md5" => "Content-MD5",
393
- }.freeze
394
-
395
394
  def capitalized(field)
396
395
  UPCASED[field] || field.split("-").map(&:capitalize).join("-")
397
396
  end
@@ -3,6 +3,8 @@
3
3
  require "securerandom"
4
4
  require "http/2"
5
5
 
6
+ HTTP2::Connection.__send__(:public, :send_buffer) if HTTP2::VERSION < "1.1.1"
7
+
6
8
  module HTTPX
7
9
  class Connection::HTTP2
8
10
  include Callbacks
@@ -100,14 +100,13 @@ module HTTPX
100
100
  def match?(uri, options)
101
101
  return false if !used? && (@state == :closing || @state == :closed)
102
102
 
103
- (
104
- @origins.include?(uri.origin) &&
103
+ @origins.include?(uri.origin) &&
105
104
  # if there is more than one origin to match, it means that this connection
106
105
  # was the result of coalescing. To prevent blind trust in the case where the
107
106
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
108
107
  # SSL certificate
109
- (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
110
- ) && @options == options
108
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
109
+ @options == options
111
110
  end
112
111
 
113
112
  def mergeable?(connection)
@@ -146,10 +145,6 @@ module HTTPX
146
145
  end
147
146
  end
148
147
 
149
- def create_idle(options = {})
150
- self.class.new(@origin, @options.merge(options))
151
- end
152
-
153
148
  def merge(connection)
154
149
  @origins |= connection.instance_variable_get(:@origins)
155
150
  if @ssl_session.nil? && connection.ssl_session
@@ -55,7 +55,7 @@ module HTTPX
55
55
  def new(domain)
56
56
  return domain if domain.is_a?(self)
57
57
 
58
- super(domain)
58
+ super
59
59
  end
60
60
 
61
61
  # Normalizes a _domain_ using the Punycode algorithm as necessary.
data/lib/httpx/headers.rb CHANGED
@@ -42,12 +42,12 @@ module HTTPX
42
42
  # dupped initialization
43
43
  def initialize_dup(orig)
44
44
  super
45
- @headers = orig.instance_variable_get(:@headers).dup
45
+ @headers = orig.instance_variable_get(:@headers).transform_values(&:dup)
46
46
  end
47
47
 
48
48
  # freezes the headers hash
49
49
  def freeze
50
- @headers.freeze
50
+ @headers.each_value(&:freeze).freeze
51
51
  super
52
52
  end
53
53
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fiber" if RUBY_VERSION < "3.0.0"
4
+
3
5
  module HTTPX
4
6
  module Loggable
5
7
  COLORS = {
data/lib/httpx/options.rb CHANGED
@@ -188,33 +188,40 @@ module HTTPX
188
188
  end
189
189
 
190
190
  def merge(other)
191
- ivar_map = nil
192
- other_ivars = case other
193
- when Options
194
- other.instance_variables
195
- else
196
- other = Hash[other] unless other.is_a?(Hash)
197
- ivar_map = other.keys.to_h { |k| [:"@#{k}", k] }
198
- ivar_map.keys
199
- end
191
+ if (is_options = other.is_a?(Options))
192
+
193
+ return self if eql?(other)
194
+
195
+ opts_names = other.class.options_names
196
+
197
+ return self if opts_names.all? { |opt| public_send(opt) == other.public_send(opt) }
200
198
 
201
- return self if other_ivars.empty?
199
+ other_opts = opts_names
200
+ else
201
+ other_opts = other # : Hash[Symbol, untyped]
202
+ other_opts = Hash[other] unless other.is_a?(Hash)
203
+
204
+ return self if other_opts.empty?
202
205
 
203
- return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == access_option(other, ivar, ivar_map) }
206
+ return self if other_opts.all? { |opt, v| !respond_to?(opt) || public_send(opt) == v }
207
+ end
204
208
 
205
209
  opts = dup
206
210
 
207
- other_ivars.each do |ivar|
208
- v = access_option(other, ivar, ivar_map)
211
+ other_opts.each do |opt, v|
212
+ next unless respond_to?(opt)
213
+
214
+ v = other.public_send(opt) if is_options
215
+ ivar = :"@#{opt}"
209
216
 
210
217
  unless v
211
218
  opts.instance_variable_set(ivar, v)
212
219
  next
213
220
  end
214
221
 
215
- v = opts.__send__(:"option_#{ivar[1..-1]}", v)
222
+ v = opts.__send__(:"option_#{opt}", v)
216
223
 
217
- orig_v = instance_variable_get(ivar)
224
+ orig_v = public_send(opt)
218
225
 
219
226
  v = orig_v.merge(v) if orig_v.respond_to?(:merge) && v.respond_to?(:merge)
220
227
 
@@ -379,7 +386,20 @@ module HTTPX
379
386
  end
380
387
 
381
388
  def option_timeout(value)
382
- Hash[value]
389
+ timeout_hash = Hash[value]
390
+
391
+ default_timeouts = DEFAULT_OPTIONS[:timeout]
392
+
393
+ # Validate keys and values
394
+ timeout_hash.each do |key, val|
395
+ raise TypeError, "invalid timeout: :#{key}" unless default_timeouts.key?(key)
396
+
397
+ next if val.nil?
398
+
399
+ raise TypeError, ":#{key} must be numeric" unless val.is_a?(Numeric)
400
+ end
401
+
402
+ timeout_hash
383
403
  end
384
404
 
385
405
  def option_supported_compression_formats(value)
@@ -424,6 +444,8 @@ module HTTPX
424
444
  end
425
445
  end
426
446
 
447
+ # rubocop:disable Lint/UselessConstantScoping
448
+ # these really need to be defined at the end of the class
427
449
  SET_TEMPORARY_NAME = ->(klass, pl = nil) do
428
450
  if klass.respond_to?(:set_temporary_name) # ruby 3.4 only
429
451
  name = klass.name || "#{klass.superclass.name}(plugin)"
@@ -478,6 +500,7 @@ module HTTPX
478
500
  :pool_options => EMPTY_HASH,
479
501
  :ip_families => nil,
480
502
  :close_on_fork => false,
481
- }.freeze
503
+ }.each_value(&:freeze).freeze
504
+ # rubocop:enable Lint/UselessConstantScoping
482
505
  end
483
506
  end
@@ -8,6 +8,8 @@ module HTTPX
8
8
  module Plugins
9
9
  module Authentication
10
10
  class Digest
11
+ Error = Class.new(Error)
12
+
11
13
  def initialize(user, password, hashed: false, **)
12
14
  @user = user
13
15
  @password = password
@@ -29,19 +31,53 @@ module HTTPX
29
31
  # discard first token, it's Digest
30
32
  auth_info = authenticate[/^(\w+) (.*)/, 2]
31
33
 
32
- params = auth_info.split(/ *, */)
33
- .to_h { |val| val.split("=", 2) }
34
- .transform_values { |v| v.delete("\"") }
34
+ raise_format_error unless auth_info
35
+
36
+ s = StringScanner.new(auth_info)
37
+
38
+ params = {}
39
+ until s.eos?
40
+ k = s.scan_until(/=/)
41
+ raise_format_error unless k&.end_with?("=")
42
+
43
+ if s.peek(1) == "\""
44
+ s.skip("\"")
45
+ v = s.scan_until(/"/)
46
+ raise_format_error unless v&.end_with?("\"")
47
+
48
+ v = v[0..-2]
49
+ s.skip_until(/,/)
50
+ else
51
+ v = s.scan_until(/,|$/)
52
+
53
+ if v&.end_with?(",")
54
+ v = v[0..-2]
55
+ else
56
+ raise_format_error unless s.eos?
57
+ end
58
+
59
+ v = v[0..-2] if v&.end_with?(",")
60
+ end
61
+ params[k[0..-2]] = v
62
+ s.skip(/\s/)
63
+ end
64
+
35
65
  nonce = params["nonce"]
36
66
  nc = next_nonce
37
67
 
38
68
  # verify qop
39
69
  qop = params["qop"]
40
70
 
71
+ if qop
72
+ # some servers send multiple values wrapped in parentheses (i.e. "(qauth,)")
73
+ qop = qop[/\(?([^)]+)\)?/, 1]
74
+ qop = qop.split(",").map { |s| s.delete_prefix("'").delete_suffix("'") }.delete_if(&:empty?).map.first
75
+ end
76
+
41
77
  if params["algorithm"] =~ /(.*?)(-sess)?$/
42
78
  alg = Regexp.last_match(1)
43
79
  algorithm = ::Digest.const_get(alg)
44
- raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
80
+ raise Error, "unknown algorithm \"#{alg}\"" unless algorithm
45
81
 
46
82
  sess = Regexp.last_match(2)
47
83
  else
@@ -96,6 +132,10 @@ module HTTPX
96
132
  def next_nonce
97
133
  @nonce += 1
98
134
  end
135
+
136
+ def raise_format_error
137
+ raise Error, "unsupported digest header format"
138
+ end
99
139
  end
100
140
  end
101
141
  end
@@ -10,13 +10,96 @@ module HTTPX
10
10
  # https://gitlab.com/os85/httpx/wikis/Auth#auth
11
11
  #
12
12
  module Auth
13
+ def self.subplugins
14
+ {
15
+ retries: AuthRetries,
16
+ }
17
+ end
18
+
19
+ module OptionsMethods
20
+ def option_auth_header_value(value)
21
+ value
22
+ end
23
+
24
+ def option_auth_header_type(value)
25
+ value
26
+ end
27
+
28
+ def option_generate_auth_value_on_retry(value)
29
+ raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
30
+
31
+ value
32
+ end
33
+ end
34
+
13
35
  module InstanceMethods
14
- def authorization(token)
15
- with(headers: { "authorization" => token })
36
+ def initialize(*)
37
+ super
38
+
39
+ @auth_header_value = nil
40
+ @skip_auth_header_value = false
41
+ end
42
+
43
+ def authorization(token = nil, auth_header_type: nil, &blk)
44
+ with(auth_header_type: auth_header_type, auth_header_value: token || blk)
45
+ end
46
+
47
+ def bearer_auth(token = nil, &blk)
48
+ authorization(token, auth_header_type: "Bearer", &blk)
49
+ end
50
+
51
+ def skip_auth_header
52
+ @skip_auth_header_value = true
53
+ yield
54
+ ensure
55
+ @skip_auth_header_value = false
56
+ end
57
+
58
+ def reset_auth_header_value!
59
+ @auth_header_value = nil
60
+ end
61
+
62
+ private
63
+
64
+ def send_request(request, *)
65
+ return super if @skip_auth_header_value
66
+
67
+ @auth_header_value ||= generate_auth_token
68
+
69
+ request.authorize(@auth_header_value) if @auth_header_value
70
+
71
+ super
72
+ end
73
+
74
+ def generate_auth_token
75
+ return unless (auth_value = @options.auth_header_value)
76
+
77
+ auth_value = auth_value.call(self) if auth_value.respond_to?(:call)
78
+
79
+ auth_value
16
80
  end
81
+ end
82
+
83
+ module RequestMethods
84
+ def authorize(auth_value)
85
+ if (auth_type = @options.auth_header_type)
86
+ auth_value = "#{auth_type} #{auth_value}"
87
+ end
88
+
89
+ @headers.add("authorization", auth_value)
90
+ end
91
+ end
92
+
93
+ module AuthRetries
94
+ module InstanceMethods
95
+ def prepare_to_retry(request, response)
96
+ super
97
+
98
+ return unless @options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response)
17
99
 
18
- def bearer_auth(token)
19
- authorization("Bearer #{token}")
100
+ request.headers.get("authorization").pop
101
+ @auth_header_value = generate_auth_token
102
+ end
20
103
  end
21
104
  end
22
105
  end
@@ -94,7 +94,6 @@ module HTTPX
94
94
  region: AwsSdkAuthentication.region(@options.aws_profile),
95
95
  **options
96
96
  )
97
-
98
97
  aws_sigv4_authentication(
99
98
  credentials: credentials,
100
99
  region: region,
@@ -7,6 +7,7 @@ module HTTPX
7
7
  # Contains the single cookie info: name, value and attributes.
8
8
  class Cookie
9
9
  include Comparable
10
+
10
11
  # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
11
12
  # least)
12
13
  MAX_LENGTH = 4096
@@ -8,15 +8,14 @@ module HTTPX
8
8
  # https://gitlab.com/os85/httpx/wikis/Auth#digest-auth
9
9
  #
10
10
  module DigestAuth
11
- DigestError = Class.new(Error)
12
-
13
11
  class << self
14
12
  def extra_options(options)
15
13
  options.merge(max_concurrent_requests: 1)
16
14
  end
17
15
 
18
- def load_dependencies(*)
16
+ def load_dependencies(klass)
19
17
  require_relative "auth/digest"
18
+ klass.plugin(:auth)
20
19
  end
21
20
  end
22
21
 
@@ -48,11 +47,11 @@ module HTTPX
48
47
 
49
48
  probe_response = wrap { super(request).first }
50
49
 
51
- return ([probe_response] * requests.size) unless probe_response.is_a?(Response)
50
+ return [probe_response] * requests.size unless probe_response.is_a?(Response)
52
51
 
53
52
  if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
54
53
  request.transition(:idle)
55
- request.headers["authorization"] = digest.authenticate(request, probe_response.headers["www-authenticate"])
54
+ request.authorize(digest.authenticate(request, probe_response.headers["www-authenticate"]))
56
55
  super(request)
57
56
  else
58
57
  probe_response