httpx 1.2.6 → 1.3.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_3_0.md +18 -0
  3. data/doc/release_notes/1_3_1.md +17 -0
  4. data/lib/httpx/adapters/datadog.rb +8 -4
  5. data/lib/httpx/adapters/faraday.rb +2 -1
  6. data/lib/httpx/adapters/webmock.rb +1 -1
  7. data/lib/httpx/connection/http1.rb +11 -7
  8. data/lib/httpx/connection/http2.rb +15 -11
  9. data/lib/httpx/connection.rb +51 -24
  10. data/lib/httpx/io/tcp.rb +1 -1
  11. data/lib/httpx/io/unix.rb +1 -1
  12. data/lib/httpx/options.rb +4 -7
  13. data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
  14. data/lib/httpx/plugins/aws_sigv4.rb +5 -1
  15. data/lib/httpx/plugins/circuit_breaker.rb +10 -0
  16. data/lib/httpx/plugins/cookies.rb +9 -6
  17. data/lib/httpx/plugins/digest_auth.rb +3 -0
  18. data/lib/httpx/plugins/expect.rb +5 -0
  19. data/lib/httpx/plugins/follow_redirects.rb +65 -29
  20. data/lib/httpx/plugins/grpc.rb +2 -2
  21. data/lib/httpx/plugins/h2c.rb +1 -1
  22. data/lib/httpx/plugins/oauth.rb +1 -1
  23. data/lib/httpx/plugins/proxy/http.rb +9 -4
  24. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  25. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  26. data/lib/httpx/plugins/proxy.rb +24 -13
  27. data/lib/httpx/plugins/retries.rb +25 -4
  28. data/lib/httpx/plugins/ssrf_filter.rb +4 -1
  29. data/lib/httpx/pool/synch_pool.rb +93 -0
  30. data/lib/httpx/pool.rb +1 -1
  31. data/lib/httpx/request/body.rb +37 -41
  32. data/lib/httpx/request.rb +42 -13
  33. data/lib/httpx/resolver/https.rb +7 -5
  34. data/lib/httpx/resolver/native.rb +1 -1
  35. data/lib/httpx/resolver/resolver.rb +1 -1
  36. data/lib/httpx/resolver/system.rb +1 -1
  37. data/lib/httpx/response.rb +2 -2
  38. data/lib/httpx/session.rb +34 -19
  39. data/lib/httpx/timers.rb +1 -1
  40. data/lib/httpx/version.rb +1 -1
  41. data/sig/chainable.rbs +2 -2
  42. data/sig/connection/http1.rbs +2 -2
  43. data/sig/connection/http2.rbs +17 -17
  44. data/sig/connection.rbs +10 -4
  45. data/sig/httpx.rbs +3 -3
  46. data/sig/io/tcp.rbs +1 -1
  47. data/sig/io/unix.rbs +1 -1
  48. data/sig/options.rbs +1 -13
  49. data/sig/plugins/follow_redirects.rbs +1 -1
  50. data/sig/plugins/proxy/http.rbs +3 -0
  51. data/sig/plugins/proxy.rbs +2 -0
  52. data/sig/plugins/push_promise.rbs +3 -3
  53. data/sig/pool.rbs +1 -1
  54. data/sig/request/body.rbs +1 -3
  55. data/sig/request.rbs +2 -1
  56. data/sig/resolver/resolver.rbs +2 -2
  57. data/sig/response.rbs +1 -1
  58. data/sig/session.rbs +11 -6
  59. metadata +11 -6
@@ -4,12 +4,17 @@ module HTTPX
4
4
  InsecureRedirectError = Class.new(Error)
5
5
  module Plugins
6
6
  #
7
- # This plugin adds support for following redirect (status 30X) responses.
7
+ # This plugin adds support for automatically following redirect (status 30X) responses.
8
8
  #
9
- # It has an upper bound of followed redirects (see *MAX_REDIRECTS*), after which it
10
- # will return the last redirect response. It will **not** raise an exception.
9
+ # It has a default upper bound of followed redirects (see *MAX_REDIRECTS* and the *max_redirects* option),
10
+ # after which it will return the last redirect response. It will **not** raise an exception.
11
11
  #
12
- # It also doesn't follow insecure redirects (https -> http) by default (see *follow_insecure_redirects*).
12
+ # It doesn't follow insecure redirects (https -> http) by default (see *follow_insecure_redirects*).
13
+ #
14
+ # It doesn't propagate authorization related headers to requests redirecting to different origins
15
+ # (see *allow_auth_to_other_origins*) to override.
16
+ #
17
+ # It allows customization of when to redirect via the *redirect_on* callback option).
13
18
  #
14
19
  # https://gitlab.com/os85/httpx/wikis/Follow-Redirects
15
20
  #
@@ -20,6 +25,14 @@ module HTTPX
20
25
 
21
26
  using URIExtensions
22
27
 
28
+ # adds support for the following options:
29
+ #
30
+ # :max_redirects :: max number of times a request will be redirected (defaults to <tt>3</tt>).
31
+ # :follow_insecure_redirects :: whether redirects to an "http://" URI, when coming from an "https//", are allowed
32
+ # (defaults to <tt>false</tt>).
33
+ # :allow_auth_to_other_origins :: whether auth-related headers, such as "Authorization", are propagated on redirection
34
+ # (defaults to <tt>false</tt>).
35
+ # :redirect_on :: optional callback which receives the redirect location and can halt the redirect chain if it returns <tt>false</tt>.
23
36
  module OptionsMethods
24
37
  def option_max_redirects(value)
25
38
  num = Integer(value)
@@ -44,6 +57,7 @@ module HTTPX
44
57
  end
45
58
 
46
59
  module InstanceMethods
60
+ # returns a session with the *max_redirects* option set to +n+
47
61
  def max_redirects(n)
48
62
  with(max_redirects: n.to_i)
49
63
  end
@@ -71,40 +85,40 @@ module HTTPX
71
85
  # build redirect request
72
86
  request_body = redirect_request.body
73
87
  redirect_method = "GET"
88
+ redirect_params = {}
74
89
 
75
90
  if response.status == 305 && options.respond_to?(:proxy)
76
91
  request_body.rewind
77
92
  # The requested resource MUST be accessed through the proxy given by
78
93
  # the Location field. The Location field gives the URI of the proxy.
79
- retry_options = options.merge(headers: redirect_request.headers,
80
- proxy: { uri: redirect_uri },
81
- body: request_body,
82
- max_redirects: max_redirects - 1)
94
+ redirect_options = options.merge(headers: redirect_request.headers,
95
+ proxy: { uri: redirect_uri },
96
+ max_redirects: max_redirects - 1)
97
+
98
+ redirect_params[:body] = request_body
83
99
  redirect_uri = redirect_request.uri
84
- options = retry_options
100
+ options = redirect_options
85
101
  else
86
102
  redirect_headers = redirect_request_headers(redirect_request.uri, redirect_uri, request.headers, options)
87
-
88
- retry_opts = Hash[options].merge(max_redirects: max_redirects - 1)
103
+ redirect_opts = Hash[options]
104
+ redirect_params[:max_redirects] = max_redirects - 1
89
105
 
90
106
  unless request_body.empty?
91
107
  if response.status == 307
92
108
  # The method and the body of the original request are reused to perform the redirected request.
93
109
  redirect_method = redirect_request.verb
94
110
  request_body.rewind
95
- retry_opts[:body] = request_body
111
+ redirect_params[:body] = request_body
96
112
  else
97
113
  # redirects are **ALWAYS** GET, so remove body-related headers
98
114
  REQUEST_BODY_HEADERS.each do |h|
99
115
  redirect_headers.delete(h)
100
116
  end
101
- retry_opts.delete(:body)
117
+ redirect_params[:body] = nil
102
118
  end
103
119
  end
104
120
 
105
- retry_opts[:headers] = redirect_headers.to_h
106
-
107
- retry_options = options.class.new(retry_opts)
121
+ options = options.class.new(redirect_opts.merge(headers: redirect_headers.to_h))
108
122
  end
109
123
 
110
124
  redirect_uri = Utils.to_uri(redirect_uri)
@@ -114,27 +128,35 @@ module HTTPX
114
128
  redirect_uri.scheme == "http"
115
129
  error = InsecureRedirectError.new(redirect_uri.to_s)
116
130
  error.set_backtrace(caller)
117
- return ErrorResponse.new(request, error, options)
131
+ return ErrorResponse.new(request, error)
118
132
  end
119
133
 
120
- retry_request = build_request(redirect_method, redirect_uri, retry_options)
134
+ retry_request = build_request(redirect_method, redirect_uri, redirect_params, options)
121
135
 
122
136
  request.redirect_request = retry_request
123
137
 
124
- retry_after = response.headers["retry-after"]
138
+ redirect_after = response.headers["retry-after"]
125
139
 
126
- if retry_after
140
+ if redirect_after
127
141
  # Servers send the "Retry-After" header field to indicate how long the
128
142
  # user agent ought to wait before making a follow-up request.
129
143
  # When sent with any 3xx (Redirection) response, Retry-After indicates
130
144
  # the minimum time that the user agent is asked to wait before issuing
131
145
  # the redirected request.
132
146
  #
133
- retry_after = Utils.parse_retry_after(retry_after)
147
+ redirect_after = Utils.parse_retry_after(redirect_after)
134
148
 
135
- log { "redirecting after #{retry_after} secs..." }
136
- pool.after(retry_after) do
137
- send_request(retry_request, connections, options)
149
+ log { "redirecting after #{redirect_after} secs..." }
150
+
151
+ deactivate_connection(request, connections, options)
152
+
153
+ pool.after(redirect_after) do
154
+ if request.response
155
+ # request has terminated abruptly meanwhile
156
+ retry_request.emit(:response, request.response)
157
+ else
158
+ send_request(retry_request, connections, options)
159
+ end
138
160
  end
139
161
  else
140
162
  send_request(retry_request, connections, options)
@@ -142,6 +164,7 @@ module HTTPX
142
164
  nil
143
165
  end
144
166
 
167
+ # :nodoc:
145
168
  def redirect_request_headers(original_uri, redirect_uri, headers, options)
146
169
  headers = headers.dup
147
170
 
@@ -149,14 +172,14 @@ module HTTPX
149
172
 
150
173
  return headers unless headers.key?("authorization")
151
174
 
152
- unless original_uri.origin == redirect_uri.origin
153
- headers = headers.dup
154
- headers.delete("authorization")
155
- end
175
+ return headers if original_uri.origin == redirect_uri.origin
176
+
177
+ headers.delete("authorization")
156
178
 
157
179
  headers
158
180
  end
159
181
 
182
+ # :nodoc:
160
183
  def __get_location_from_response(response)
161
184
  # @type var location_uri: http_uri
162
185
  location_uri = URI(response.headers["location"])
@@ -166,12 +189,15 @@ module HTTPX
166
189
  end
167
190
 
168
191
  module RequestMethods
192
+ # returns the top-most original HTTPX::Request from the redirect chain
169
193
  attr_accessor :root_request
170
194
 
195
+ # returns the follow-up redirect request, or itself
171
196
  def redirect_request
172
197
  @redirect_request || self
173
198
  end
174
199
 
200
+ # sets the follow-up redirect request
175
201
  def redirect_request=(req)
176
202
  @redirect_request = req
177
203
  req.root_request = @root_request || self
@@ -179,7 +205,7 @@ module HTTPX
179
205
  end
180
206
 
181
207
  def response
182
- return super unless @redirect_request
208
+ return super unless @redirect_request && @response.nil?
183
209
 
184
210
  @redirect_request.response
185
211
  end
@@ -188,6 +214,16 @@ module HTTPX
188
214
  @options.max_redirects || MAX_REDIRECTS
189
215
  end
190
216
  end
217
+
218
+ module ConnectionMethods
219
+ private
220
+
221
+ def set_request_request_timeout(request)
222
+ return unless request.root_request.nil?
223
+
224
+ super
225
+ end
226
+ end
191
227
  end
192
228
  register_plugin :follow_redirects, FollowRedirects
193
229
  end
@@ -110,10 +110,10 @@ module HTTPX
110
110
  end
111
111
 
112
112
  module RequestBodyMethods
113
- def initialize(headers, _)
113
+ def initialize(*, **)
114
114
  super
115
115
 
116
- if (compression = headers["grpc-encoding"])
116
+ if (compression = @headers["grpc-encoding"])
117
117
  deflater_body = self.class.initialize_deflater_body(@body, compression)
118
118
  @body = Transcoder::GRPCEncoding.encode(deflater_body || @body, compressed: !deflater_body.nil?)
119
119
  else
@@ -39,7 +39,7 @@ module HTTPX
39
39
  upgrade_request.headers.add("connection", "upgrade")
40
40
  upgrade_request.headers.add("connection", "http2-settings")
41
41
  upgrade_request.headers["upgrade"] = "h2c"
42
- upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
42
+ upgrade_request.headers["http2-settings"] = ::HTTP2::Client.settings_header(upgrade_request.options.http2_settings)
43
43
 
44
44
  super(upgrade_request, *remainder)
45
45
  end
@@ -155,7 +155,7 @@ module HTTPX
155
155
  with(oauth_session: oauth_session.merge(access_token: access_token, refresh_token: refresh_token))
156
156
  end
157
157
 
158
- def build_request(*, _)
158
+ def build_request(*)
159
159
  request = super
160
160
 
161
161
  return request if request.headers.key?("authorization")
@@ -32,9 +32,14 @@ module HTTPX
32
32
  !request.headers.key?("proxy-authorization") &&
33
33
  response.headers.key?("proxy-authenticate")
34
34
 
35
- connection = find_connection(request, connections, options)
35
+ uri = request.uri
36
36
 
37
- if connection.options.proxy.can_authenticate?(response.headers["proxy-authenticate"])
37
+ proxy_options = proxy_options(uri, options)
38
+ connection = connections.find do |conn|
39
+ conn.match?(uri, proxy_options)
40
+ end
41
+
42
+ if connection && connection.options.proxy.can_authenticate?(response.headers["proxy-authenticate"])
38
43
  request.transition(:idle)
39
44
  request.headers["proxy-authorization"] =
40
45
  connection.options.proxy.authenticate(request, response.headers["proxy-authenticate"])
@@ -163,8 +168,8 @@ module HTTPX
163
168
  end
164
169
 
165
170
  class ConnectRequest < Request
166
- def initialize(uri, _options)
167
- super("CONNECT", uri, {})
171
+ def initialize(uri, options)
172
+ super("CONNECT", uri, options)
168
173
  @headers.delete("accept")
169
174
  end
170
175
 
@@ -89,7 +89,7 @@ module HTTPX
89
89
 
90
90
  def initialize(buffer, options)
91
91
  @buffer = buffer
92
- @options = Options.new(options)
92
+ @options = options
93
93
  end
94
94
 
95
95
  def close; end
@@ -141,7 +141,7 @@ module HTTPX
141
141
 
142
142
  def initialize(buffer, options)
143
143
  @buffer = buffer
144
- @options = Options.new(options)
144
+ @options = options
145
145
  end
146
146
 
147
147
  def close; end
@@ -89,6 +89,10 @@ module HTTPX
89
89
  end
90
90
  end
91
91
 
92
+ # adds support for the following options:
93
+ #
94
+ # :proxy :: proxy options defining *:uri*, *:username*, *:password* or
95
+ # *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
92
96
  module OptionsMethods
93
97
  def option_proxy(value)
94
98
  value.is_a?(Parameters) ? value : Hash[value]
@@ -107,16 +111,29 @@ module HTTPX
107
111
  def find_connection(request, connections, options)
108
112
  return super unless options.respond_to?(:proxy)
109
113
 
110
- uri = URI(request.uri)
114
+ uri = request.uri
111
115
 
112
- proxy_opts = if (next_proxy = uri.find_proxy)
116
+ proxy_options = proxy_options(uri, options)
117
+
118
+ return super(request, connections, proxy_options) unless proxy_options.proxy
119
+
120
+ connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
121
+ unless connections.nil? || connections.include?(connection)
122
+ connections << connection
123
+ set_connection_callbacks(connection, connections, options)
124
+ end
125
+ connection
126
+ end
127
+
128
+ def proxy_options(request_uri, options)
129
+ proxy_opts = if (next_proxy = request_uri.find_proxy)
113
130
  { uri: next_proxy }
114
131
  else
115
132
  proxy = options.proxy
116
133
 
117
- return super unless proxy
134
+ return options unless proxy
118
135
 
119
- return super(request, connections, options.merge(proxy: nil)) unless proxy.key?(:uri)
136
+ return options.merge(proxy: nil) unless proxy.key?(:uri)
120
137
 
121
138
  @_proxy_uris ||= Array(proxy[:uri])
122
139
 
@@ -133,8 +150,8 @@ module HTTPX
133
150
  no_proxy = proxy[:no_proxy]
134
151
  no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
135
152
 
136
- return super(request, connections, options.merge(proxy: nil)) unless URI::Generic.use_proxy?(uri.host, next_proxy.host,
137
- next_proxy.port, no_proxy)
153
+ return options.merge(proxy: nil) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
154
+ next_proxy.port, no_proxy)
138
155
  end
139
156
 
140
157
  proxy.merge(uri: next_proxy)
@@ -142,13 +159,7 @@ module HTTPX
142
159
 
143
160
  proxy = Parameters.new(**proxy_opts)
144
161
 
145
- proxy_options = options.merge(proxy: proxy)
146
- connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
147
- unless connections.nil? || connections.include?(connection)
148
- connections << connection
149
- set_connection_callbacks(connection, connections, options)
150
- end
151
- connection
162
+ options.merge(proxy: proxy)
152
163
  end
153
164
 
154
165
  def fetch_response(request, connections, options)
@@ -3,7 +3,12 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  #
6
- # This plugin adds support for retrying requests when certain errors happen.
6
+ # This plugin adds support for retrying requests when errors happen.
7
+ #
8
+ # It has a default max number of retries (see *MAX_RETRIES* and the *max_retries* option),
9
+ # after which it will return the last response, error or not. It will **not** raise an exception.
10
+ #
11
+ # It does not retry which are not considered idempotent (see *retry_change_requests* to override).
7
12
  #
8
13
  # https://gitlab.com/os85/httpx/wikis/Retries
9
14
  #
@@ -38,6 +43,14 @@ module HTTPX
38
43
  end
39
44
  end
40
45
 
46
+ # adds support for the following options:
47
+ #
48
+ # :max_retries :: max number of times a request will be retried (defaults to <tt>3</tt>).
49
+ # :retry_change_requests :: whether idempotent requests are retried (defaults to <tt>false</tt>).
50
+ # :retry_after:: seconds after which a request is retried; can also be a callable object (i.e. <tt>->(req, res) { ... } </tt>)
51
+ # :retry_jitter :: number of seconds applied to *:retry_after* (must be a callable, i.e. <tt>->(retry_after) { ... } </tt>).
52
+ # :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
53
+ # (i.e. <tt>->(res) { ... }</tt>).
41
54
  module OptionsMethods
42
55
  def option_retry_after(value)
43
56
  # return early if callable
@@ -76,7 +89,7 @@ module HTTPX
76
89
 
77
90
  module InstanceMethods
78
91
  def max_retries(n)
79
- with(max_retries: n.to_i)
92
+ with(max_retries: n)
80
93
  end
81
94
 
82
95
  private
@@ -111,9 +124,17 @@ module HTTPX
111
124
 
112
125
  retry_start = Utils.now
113
126
  log { "retrying after #{retry_after} secs..." }
127
+
128
+ deactivate_connection(request, connections, options)
129
+
114
130
  pool.after(retry_after) do
115
- log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
116
- send_request(request, connections, options)
131
+ if request.response
132
+ # request has terminated abruptly meanwhile
133
+ request.emit(:response, request.response)
134
+ else
135
+ log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
136
+ send_request(request, connections, options)
137
+ end
117
138
  end
118
139
  else
119
140
  send_request(request, connections, options)
@@ -87,6 +87,9 @@ module HTTPX
87
87
  end
88
88
  end
89
89
 
90
+ # adds support for the following options:
91
+ #
92
+ # :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
90
93
  module OptionsMethods
91
94
  def option_allowed_schemes(value)
92
95
  Array(value)
@@ -100,7 +103,7 @@ module HTTPX
100
103
 
101
104
  error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
102
105
  error.set_backtrace(caller)
103
- response = ErrorResponse.new(request, error, request.options)
106
+ response = ErrorResponse.new(request, error)
104
107
  request.emit(:response, response)
105
108
  response
106
109
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thread"
4
+
5
+ module HTTPX
6
+ class SynchPool < Pool
7
+ def initialize(options)
8
+ super
9
+
10
+ @connections = ConnectionStore.new(options)
11
+ end
12
+
13
+ # TODO: #wrap
14
+ def find_or_new_connection(uri, options, &blk)
15
+ @connections.find_or_new(uri, options) do |new_conn|
16
+ catch(:coalesced) do
17
+ init_connection(new_conn, options)
18
+ blk.call(new_conn) if blk
19
+ new_conn
20
+ end
21
+ end
22
+ find_connection(uri, options) || new_connection(uri, options, &blk)
23
+ end
24
+
25
+ class ConnectionManager
26
+ include Enumerable
27
+
28
+ def initialize(limit = 3)
29
+ @connections = []
30
+ @used = 0
31
+ @limit = limit
32
+ end
33
+
34
+ def each(*args, &blk)
35
+ @connections.each(*args, &blk)
36
+ end
37
+
38
+ def find_or_new(uri, options, &blk)
39
+ raise "over limit" if @used >= @limit
40
+
41
+ @used += 1
42
+ conn = @connections.find do |connection|
43
+ connection.match?(uri, options)
44
+ end
45
+
46
+ if conn
47
+ @connections.delete(conn)
48
+ else
49
+ conn = options.connection_class.new(uri, options)
50
+ blk[conn]
51
+ end
52
+
53
+ conn
54
+ end
55
+ end
56
+
57
+ class ConnectionStore
58
+ include Enumerable
59
+
60
+ def initialize(options)
61
+ @connections = Hash.new { |hs, k| hs[k] ||= ConnectionManager.new }
62
+ @conn_mtx = Thread::Mutex.new
63
+ @conn_waiter = ConditionVariable.new
64
+ @timeout = Float(options.fetch(:pool_timeout, 5))
65
+ end
66
+
67
+ def each(&block)
68
+ return enum_for(__meth__) unless block
69
+
70
+ @conn_mtx.synchronize do
71
+ @connections.each_value do |conns|
72
+ conns.each(&block)
73
+ end
74
+ end
75
+ end
76
+
77
+ def find_or_new(uri, options, &blk)
78
+ @connections[uri.origin].find_or_new(uri, options, &blk)
79
+ end
80
+
81
+ # def <<(conn)
82
+ # @conn_mtx.synchronize do
83
+ # origin, conns = @connections.find { |_orig, _| conn.origins.include?(origin) }
84
+ # (conns || @connections[conn.origin.to_s]) << conn
85
+ # end
86
+ # end
87
+
88
+ def empty?
89
+ @conn_mtx.synchronize { super }
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/httpx/pool.rb CHANGED
@@ -108,7 +108,7 @@ module HTTPX
108
108
  resolve_connection(connection) unless connection.family
109
109
  end
110
110
 
111
- def deactivate(connections)
111
+ def deactivate(*connections)
112
112
  connections.each do |connection|
113
113
  connection.deactivate
114
114
  deselect_connection(connection) if connection.state == :inactive
@@ -4,30 +4,53 @@ module HTTPX
4
4
  # Implementation of the HTTP Request body as a delegator which iterates (responds to +each+) payload chunks.
5
5
  class Request::Body < SimpleDelegator
6
6
  class << self
7
- def new(_, options)
8
- return options.body if options.body.is_a?(self)
7
+ def new(_, options, body: nil, **params)
8
+ if body.is_a?(self)
9
+ # request derives its options from body
10
+ body.options = options.merge(params)
11
+ return body
12
+ end
9
13
 
10
14
  super
11
15
  end
12
16
  end
13
17
 
14
- # inits the instance with the request +headers+ and +options+, which contain the payload definition.
15
- def initialize(headers, options)
16
- @headers = headers
18
+ attr_accessor :options
17
19
 
18
- # forego compression in the Range request case
19
- if @headers.key?("range")
20
- @headers.delete("accept-encoding")
21
- else
22
- @headers["accept-encoding"] ||= options.supported_compression_formats
20
+ # inits the instance with the request +headers+, +options+ and +params+, which contain the payload definition.
21
+ # it wraps the given body with the appropriate encoder on initialization.
22
+ #
23
+ # ..., json: { foo: "bar" }) #=> json encoder
24
+ # ..., form: { foo: "bar" }) #=> form urlencoded encoder
25
+ # ..., form: { foo: Pathname.open("path/to/file") }) #=> multipart urlencoded encoder
26
+ # ..., form: { foo: File.open("path/to/file") }) #=> multipart urlencoded encoder
27
+ # ..., form: { body: "bla") }) #=> raw data encoder
28
+ def initialize(headers, options, body: nil, form: nil, json: nil, xml: nil, **params)
29
+ @headers = headers
30
+ @options = options.merge(params)
31
+
32
+ @body = if body
33
+ Transcoder::Body.encode(body)
34
+ elsif form
35
+ Transcoder::Form.encode(form)
36
+ elsif json
37
+ Transcoder::JSON.encode(json)
38
+ elsif xml
39
+ Transcoder::Xml.encode(xml)
23
40
  end
24
41
 
25
- initialize_body(options)
42
+ if @body
43
+ if @options.compress_request_body && @headers.key?("content-encoding")
26
44
 
27
- return if @body.nil?
45
+ @headers.get("content-encoding").each do |encoding|
46
+ @body = self.class.initialize_deflater_body(@body, encoding)
47
+ end
48
+ end
49
+
50
+ @headers["content-type"] ||= @body.content_type
51
+ @headers["content-length"] = @body.bytesize unless unbounded_body?
52
+ end
28
53
 
29
- @headers["content-type"] ||= @body.content_type
30
- @headers["content-length"] = @body.bytesize unless unbounded_body?
31
54
  super(@body)
32
55
  end
33
56
 
@@ -99,33 +122,6 @@ module HTTPX
99
122
  end
100
123
  # :nocov:
101
124
 
102
- private
103
-
104
- # wraps the given body with the appropriate encoder.
105
- #
106
- # ..., json: { foo: "bar" }) #=> json encoder
107
- # ..., form: { foo: "bar" }) #=> form urlencoded encoder
108
- # ..., form: { foo: Pathname.open("path/to/file") }) #=> multipart urlencoded encoder
109
- # ..., form: { foo: File.open("path/to/file") }) #=> multipart urlencoded encoder
110
- # ..., form: { body: "bla") }) #=> raw data encoder
111
- def initialize_body(options)
112
- @body = if options.body
113
- Transcoder::Body.encode(options.body)
114
- elsif options.form
115
- Transcoder::Form.encode(options.form)
116
- elsif options.json
117
- Transcoder::JSON.encode(options.json)
118
- elsif options.xml
119
- Transcoder::Xml.encode(options.xml)
120
- end
121
-
122
- return unless @body && options.compress_request_body && @headers.key?("content-encoding")
123
-
124
- @headers.get("content-encoding").each do |encoding|
125
- @body = self.class.initialize_deflater_body(@body, encoding)
126
- end
127
- end
128
-
129
125
  class << self
130
126
  # returns the +body+ wrapped with the correct deflater accordinng to the given +encodisng+.
131
127
  def initialize_deflater_body(body, encoding)