httpx 1.2.6 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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)