httpx 0.15.3 → 0.17.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_15_4.md +5 -0
  3. data/doc/release_notes/0_16_0.md +93 -0
  4. data/doc/release_notes/0_16_1.md +5 -0
  5. data/doc/release_notes/0_17_0.md +49 -0
  6. data/lib/httpx/adapters/faraday.rb +3 -11
  7. data/lib/httpx/adapters/webmock.rb +2 -2
  8. data/lib/httpx/buffer.rb +1 -1
  9. data/lib/httpx/callbacks.rb +1 -1
  10. data/lib/httpx/chainable.rb +15 -8
  11. data/lib/httpx/connection/http1.rb +18 -10
  12. data/lib/httpx/connection/http2.rb +14 -21
  13. data/lib/httpx/connection.rb +6 -7
  14. data/lib/httpx/errors.rb +11 -11
  15. data/lib/httpx/headers.rb +1 -1
  16. data/lib/httpx/io/ssl.rb +2 -2
  17. data/lib/httpx/io/tls.rb +1 -1
  18. data/lib/httpx/options.rb +108 -81
  19. data/lib/httpx/parser/http1.rb +11 -7
  20. data/lib/httpx/plugins/aws_sigv4.rb +10 -9
  21. data/lib/httpx/plugins/compression.rb +12 -11
  22. data/lib/httpx/plugins/cookies/cookie.rb +4 -2
  23. data/lib/httpx/plugins/cookies/jar.rb +20 -1
  24. data/lib/httpx/plugins/cookies.rb +20 -7
  25. data/lib/httpx/plugins/digest_authentication.rb +19 -15
  26. data/lib/httpx/plugins/expect.rb +19 -15
  27. data/lib/httpx/plugins/follow_redirects.rb +9 -9
  28. data/lib/httpx/plugins/grpc/call.rb +4 -1
  29. data/lib/httpx/plugins/grpc.rb +73 -47
  30. data/lib/httpx/plugins/h2c.rb +7 -3
  31. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  32. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  33. data/lib/httpx/plugins/multipart/part.rb +2 -2
  34. data/lib/httpx/plugins/multipart.rb +14 -0
  35. data/lib/httpx/plugins/ntlm_authentication.rb +12 -10
  36. data/lib/httpx/plugins/proxy/socks4.rb +2 -1
  37. data/lib/httpx/plugins/proxy/socks5.rb +2 -1
  38. data/lib/httpx/plugins/proxy/ssh.rb +20 -13
  39. data/lib/httpx/plugins/proxy.rb +10 -10
  40. data/lib/httpx/plugins/retries.rb +25 -21
  41. data/lib/httpx/plugins/stream.rb +2 -3
  42. data/lib/httpx/plugins/upgrade.rb +7 -6
  43. data/lib/httpx/registry.rb +2 -2
  44. data/lib/httpx/request.rb +10 -19
  45. data/lib/httpx/resolver/https.rb +0 -2
  46. data/lib/httpx/resolver/native.rb +15 -3
  47. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  48. data/lib/httpx/response.rb +72 -38
  49. data/lib/httpx/selector.rb +6 -7
  50. data/lib/httpx/session.rb +34 -21
  51. data/lib/httpx/session2.rb +23 -0
  52. data/lib/httpx/transcoder/body.rb +1 -1
  53. data/lib/httpx/transcoder/chunker.rb +2 -1
  54. data/lib/httpx/transcoder/form.rb +20 -0
  55. data/lib/httpx/transcoder/json.rb +12 -0
  56. data/lib/httpx/transcoder.rb +62 -1
  57. data/lib/httpx/utils.rb +2 -2
  58. data/lib/httpx/version.rb +1 -1
  59. data/lib/httpx.rb +6 -3
  60. data/sig/buffer.rbs +3 -1
  61. data/sig/chainable.rbs +30 -29
  62. data/sig/connection/http1.rbs +11 -5
  63. data/sig/connection/http2.rbs +16 -5
  64. data/sig/connection.rbs +23 -11
  65. data/sig/errors.rbs +35 -1
  66. data/sig/headers.rbs +20 -19
  67. data/sig/httpx.rbs +4 -1
  68. data/sig/loggable.rbs +3 -1
  69. data/sig/options.rbs +45 -34
  70. data/sig/parser/http1.rbs +3 -3
  71. data/sig/plugins/authentication.rbs +1 -1
  72. data/sig/plugins/aws_sdk_authentication.rbs +5 -1
  73. data/sig/plugins/aws_sigv4.rbs +13 -5
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/compression.rbs +4 -6
  76. data/sig/plugins/cookies/cookie.rbs +5 -7
  77. data/sig/plugins/cookies/jar.rbs +9 -10
  78. data/sig/plugins/cookies.rbs +4 -5
  79. data/sig/plugins/digest_authentication.rbs +2 -3
  80. data/sig/plugins/expect.rbs +2 -4
  81. data/sig/plugins/follow_redirects.rbs +3 -5
  82. data/sig/plugins/grpc.rbs +4 -7
  83. data/sig/plugins/h2c.rbs +0 -2
  84. data/sig/plugins/multipart.rbs +64 -10
  85. data/sig/plugins/ntlm_authentication.rbs +2 -3
  86. data/sig/plugins/persistent.rbs +3 -8
  87. data/sig/plugins/proxy/ssh.rbs +4 -4
  88. data/sig/plugins/proxy.rbs +13 -13
  89. data/sig/plugins/push_promise.rbs +0 -2
  90. data/sig/plugins/retries.rbs +4 -8
  91. data/sig/plugins/stream.rbs +1 -1
  92. data/sig/plugins/upgrade.rbs +2 -3
  93. data/sig/pool.rbs +1 -2
  94. data/sig/registry.rbs +1 -1
  95. data/sig/request.rbs +11 -8
  96. data/sig/resolver/native.rbs +12 -6
  97. data/sig/resolver/resolver_mixin.rbs +4 -5
  98. data/sig/resolver/system.rbs +2 -0
  99. data/sig/resolver.rbs +7 -0
  100. data/sig/response.rbs +24 -12
  101. data/sig/selector.rbs +11 -9
  102. data/sig/session.rbs +22 -23
  103. data/sig/transcoder/body.rbs +6 -1
  104. data/sig/transcoder/chunker.rbs +8 -2
  105. data/sig/transcoder/form.rbs +3 -1
  106. data/sig/transcoder/json.rbs +2 -0
  107. data/sig/transcoder.rbs +13 -5
  108. data/sig/utils.rbs +2 -0
  109. metadata +12 -2
@@ -17,39 +17,43 @@ module HTTPX
17
17
  Errno::ECONNRESET,
18
18
  Errno::ECONNABORTED,
19
19
  Errno::EPIPE,
20
- (TLSError if defined?(TLSError)),
20
+ TLSError,
21
21
  TimeoutError,
22
22
  Parser::Error,
23
23
  Errno::EINVAL,
24
24
  Errno::ETIMEDOUT].freeze
25
25
 
26
26
  def self.extra_options(options)
27
- Class.new(options.class) do
28
- def_option(:retry_after, <<-OUT)
29
- # return early if callable
30
- unless value.respond_to?(:call)
31
- value = Integer(value)
32
- raise Error, ":retry_after must be positive" unless value.positive?
33
- end
27
+ options.merge(max_retries: MAX_RETRIES)
28
+ end
29
+
30
+ module OptionsMethods
31
+ def option_retry_after(value)
32
+ # return early if callable
33
+ unless value.respond_to?(:call)
34
+ value = Integer(value)
35
+ raise TypeError, ":retry_after must be positive" unless value.positive?
36
+ end
34
37
 
35
- value
36
- OUT
38
+ value
39
+ end
37
40
 
38
- def_option(:max_retries, <<-OUT)
39
- num = Integer(value)
40
- raise Error, ":max_retries must be positive" unless num.positive?
41
+ def option_max_retries(value)
42
+ num = Integer(value)
43
+ raise TypeError, ":max_retries must be positive" unless num.positive?
41
44
 
42
- num
43
- OUT
45
+ num
46
+ end
44
47
 
45
- def_option(:retry_change_requests)
48
+ def option_retry_change_requests(v)
49
+ v
50
+ end
46
51
 
47
- def_option(:retry_on, <<-OUT)
48
- raise ":retry_on must be called with the response" unless value.respond_to?(:call)
52
+ def option_retry_on(value)
53
+ raise ":retry_on must be called with the response" unless value.respond_to?(:call)
49
54
 
50
- value
51
- OUT
52
- end.new(options).merge(max_retries: MAX_RETRIES)
55
+ value
56
+ end
53
57
  end
54
58
 
55
59
  module InstanceMethods
@@ -6,7 +6,6 @@ module HTTPX
6
6
  @request = request
7
7
  @session = session
8
8
  @connections = connections
9
- @options = @request.options
10
9
  end
11
10
 
12
11
  def each(&block)
@@ -72,7 +71,7 @@ module HTTPX
72
71
  private
73
72
 
74
73
  def response
75
- @session.__send__(:receive_requests, [@request], @connections, @options) until @request.response
74
+ @session.__send__(:receive_requests, [@request], @connections) until @request.response
76
75
 
77
76
  @request.response
78
77
  end
@@ -106,7 +105,7 @@ module HTTPX
106
105
 
107
106
  request = requests.first
108
107
 
109
- connections = _send_requests(requests, request.options)
108
+ connections = _send_requests(requests)
110
109
 
111
110
  StreamResponse.new(request, self, connections)
112
111
  end
@@ -18,14 +18,15 @@ module HTTPX
18
18
  upgrade_handlers = Module.new do
19
19
  extend Registry
20
20
  end
21
+ options.merge(upgrade_handlers: upgrade_handlers)
22
+ end
23
+ end
21
24
 
22
- Class.new(options.class) do
23
- def_option(:upgrade_handlers, <<-OUT)
24
- raise Error, ":upgrade_handlers must be a registry" unless value.respond_to?(:registry)
25
+ module OptionsMethods
26
+ def option_upgrade_handlers(value)
27
+ raise TypeError, ":upgrade_handlers must be a registry" unless value.respond_to?(:registry)
25
28
 
26
- value
27
- OUT
28
- end.new(options).merge(upgrade_handlers: upgrade_handlers)
29
+ value
29
30
  end
30
31
  end
31
32
 
@@ -31,7 +31,7 @@ module HTTPX
31
31
  #
32
32
  module Registry
33
33
  # Base Registry Error
34
- Error = Class.new(Error)
34
+ class Error < Error; end
35
35
 
36
36
  def self.extended(klass)
37
37
  super
@@ -59,7 +59,7 @@ module HTTPX
59
59
  @registry ||= {}
60
60
  return @registry if tag.nil?
61
61
 
62
- handler = @registry.fetch(tag)
62
+ handler = @registry[tag]
63
63
  raise(Error, "#{tag} is not registered in #{self}") unless handler
64
64
 
65
65
  handler
data/lib/httpx/request.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
3
4
  require "forwardable"
4
5
 
5
6
  module HTTPX
@@ -40,16 +41,15 @@ module HTTPX
40
41
 
41
42
  def_delegator :@body, :empty?
42
43
 
43
- def_delegator :@body, :chunk!
44
-
45
44
  def initialize(verb, uri, options = {})
46
45
  @verb = verb.to_s.downcase.to_sym
47
46
  @options = Options.new(options)
48
47
  @uri = Utils.to_uri(uri)
49
48
  if @uri.relative?
50
- raise(Error, "invalid URI: #{@uri}") unless @options.origin
49
+ origin = @options.origin
50
+ raise(Error, "invalid URI: #{@uri}") unless origin
51
51
 
52
- @uri = @options.origin.merge(@uri)
52
+ @uri = origin.merge(@uri)
53
53
  end
54
54
 
55
55
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
@@ -97,7 +97,7 @@ module HTTPX
97
97
  def response=(response)
98
98
  return unless response
99
99
 
100
- if response.status == 100
100
+ if response.is_a?(Response) && response.status == 100
101
101
  @informational_status = response.status
102
102
  return
103
103
  end
@@ -155,9 +155,9 @@ module HTTPX
155
155
  end
156
156
  # :nocov:
157
157
 
158
- class Body
158
+ class Body < SimpleDelegator
159
159
  class << self
160
- def new(*, options)
160
+ def new(_, options)
161
161
  return options.body if options.body.is_a?(self)
162
162
 
163
163
  super
@@ -177,6 +177,7 @@ module HTTPX
177
177
 
178
178
  @headers["content-type"] ||= @body.content_type
179
179
  @headers["content-length"] = @body.bytesize unless unbounded_body?
180
+ super(@body)
180
181
  end
181
182
 
182
183
  def each(&block)
@@ -214,14 +215,14 @@ module HTTPX
214
215
 
215
216
  def stream(body)
216
217
  encoded = body
217
- encoded = Transcoder.registry("chunker").encode(body) if chunked?
218
+ encoded = Transcoder.registry("chunker").encode(body.enum_for(:each)) if chunked?
218
219
  encoded
219
220
  end
220
221
 
221
222
  def unbounded_body?
222
223
  return @unbounded_body if defined?(@unbounded_body)
223
224
 
224
- @unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
225
+ @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
225
226
  end
226
227
 
227
228
  def chunked?
@@ -238,16 +239,6 @@ module HTTPX
238
239
  "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
240
  end
240
241
  # :nocov:
241
-
242
- def respond_to_missing?(meth, *args)
243
- @body.respond_to?(meth, *args) || super
244
- end
245
-
246
- def method_missing(meth, *args, &block)
247
- return super unless @body.respond_to?(meth)
248
-
249
- @body.__send__(meth, *args, &block)
250
- end
251
242
  end
252
243
 
253
244
  def transition(nextstate)
@@ -24,8 +24,6 @@ module HTTPX
24
24
  record_types: RECORD_TYPES.keys,
25
25
  }.freeze
26
26
 
27
- def_delegator :@connections, :empty?
28
-
29
27
  def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
30
28
 
31
29
  def initialize(options)
@@ -217,15 +217,27 @@ module HTTPX
217
217
  end
218
218
  else
219
219
  address = addresses.first
220
- connection = @queries.delete(address["name"])
221
- return unless connection # probably a retried query for which there's an answer
220
+ name = address["name"]
221
+
222
+ connection = @queries.delete(name)
223
+
224
+ unless connection
225
+ # absolute name
226
+ name_labels = Resolv::DNS::Name.create(name).to_a
227
+ name = @queries.keys.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
228
+
229
+ # probably a retried query for which there's an answer
230
+ return unless name
231
+
232
+ address["name"] = name
233
+ connection = @queries.delete(name)
234
+ end
222
235
 
223
236
  if address.key?("alias") # CNAME
224
237
  if early_resolve(connection, hostname: address["alias"])
225
238
  @connections.delete(connection)
226
239
  else
227
240
  resolve(connection, address["alias"])
228
- @queries.delete(address["name"])
229
241
  return
230
242
  end
231
243
  else
@@ -9,7 +9,7 @@ module HTTPX
9
9
  include Callbacks
10
10
  include Loggable
11
11
 
12
- CHECK_IF_IP = proc do |name|
12
+ CHECK_IF_IP = lambda do |name|
13
13
  begin
14
14
  IPAddr.new(name)
15
15
  true
@@ -55,6 +55,7 @@ module HTTPX
55
55
  return if ips.empty?
56
56
 
57
57
  ips.map { |ip| IPAddr.new(ip) }
58
+ rescue IOError
58
59
  end
59
60
 
60
61
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "objspace"
3
4
  require "stringio"
4
5
  require "tempfile"
5
6
  require "fileutils"
@@ -13,6 +14,8 @@ module HTTPX
13
14
 
14
15
  def_delegator :@body, :to_s
15
16
 
17
+ def_delegator :@body, :to_str
18
+
16
19
  def_delegator :@body, :read
17
20
 
18
21
  def_delegator :@body, :copy_to
@@ -44,7 +47,7 @@ module HTTPX
44
47
  end
45
48
 
46
49
  def content_type
47
- ContentType.parse(@headers["content-type"])
50
+ @content_type ||= ContentType.new(@headers["content-type"])
48
51
  end
49
52
 
50
53
  def complete?
@@ -67,8 +70,31 @@ module HTTPX
67
70
  raise HTTPError, self
68
71
  end
69
72
 
73
+ def json(options = nil)
74
+ decode("json", options)
75
+ end
76
+
77
+ def form
78
+ decode("form")
79
+ end
80
+
70
81
  private
71
82
 
83
+ def decode(format, options = nil)
84
+ # TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
85
+ transcoder = Transcoder.registry(format)
86
+
87
+ raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decode)
88
+
89
+ decoder = transcoder.decode(self)
90
+
91
+ raise Error, "no decoder available for \"#{format}\"" unless decoder
92
+
93
+ decoder.call(self, options)
94
+ rescue Registry::Error
95
+ raise Error, "no decoder available for \"#{format}\""
96
+ end
97
+
72
98
  def no_data?
73
99
  @status < 200 ||
74
100
  @status == 204 ||
@@ -134,18 +160,26 @@ module HTTPX
134
160
  end
135
161
 
136
162
  def to_s
137
- rewind
138
- if @buffer
139
- content = @buffer.read
163
+ case @buffer
164
+ when StringIO
140
165
  begin
141
- return content.force_encoding(@encoding)
166
+ @buffer.string.force_encoding(@encoding)
167
+ rescue ArgumentError
168
+ @buffer.string
169
+ end
170
+ when Tempfile, File
171
+ rewind
172
+ content = _with_same_buffer_pos { @buffer.read }
173
+ begin
174
+ content.force_encoding(@encoding)
142
175
  rescue ArgumentError # ex: unknown encoding name - utf
143
- return content
176
+ content
144
177
  end
178
+ when nil
179
+ "".b
180
+ else
181
+ @buffer
145
182
  end
146
- "".b
147
- ensure
148
- close
149
183
  end
150
184
  alias_method :to_str, :to_s
151
185
 
@@ -177,7 +211,11 @@ module HTTPX
177
211
  end
178
212
 
179
213
  def ==(other)
180
- to_s == other.to_s
214
+ if other.respond_to?(:read)
215
+ _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
216
+ else
217
+ to_s == other.to_s
218
+ end
181
219
  end
182
220
 
183
221
  # :nocov:
@@ -204,7 +242,7 @@ module HTTPX
204
242
  @buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
205
243
  else
206
244
  @state = :memory
207
- @buffer = StringIO.new("".b, File::RDWR)
245
+ @buffer = StringIO.new("".b)
208
246
  end
209
247
  when :memory
210
248
  if @length > @threshold_size
@@ -222,6 +260,18 @@ module HTTPX
222
260
 
223
261
  return unless %i[memory buffer].include?(@state)
224
262
  end
263
+
264
+ def _with_same_buffer_pos
265
+ return yield unless @buffer && @buffer.respond_to?(:pos)
266
+
267
+ current_pos = @buffer.pos
268
+ @buffer.rewind
269
+ begin
270
+ yield
271
+ rescue StandardError
272
+ @buffer.pos = current_pos
273
+ end
274
+ end
225
275
  end
226
276
  end
227
277
 
@@ -229,30 +279,22 @@ module HTTPX
229
279
  MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
230
280
  CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
231
281
 
232
- attr_reader :mime_type, :charset
233
-
234
- def initialize(mime_type, charset)
235
- @mime_type = mime_type
236
- @charset = charset
282
+ def initialize(header_value)
283
+ @header_value = header_value
237
284
  end
238
285
 
239
- class << self
240
- # Parse string and return ContentType struct
241
- def parse(str)
242
- new(mime_type(str), charset(str))
243
- end
286
+ def mime_type
287
+ return @mime_type if defined?(@mime_type)
244
288
 
245
- private
289
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
290
+ m && @mime_type = m.strip.downcase
291
+ end
246
292
 
247
- def mime_type(str)
248
- m = str.to_s[MIME_TYPE_RE, 1]
249
- m && m.strip.downcase
250
- end
293
+ def charset
294
+ return @charset if defined?(@charset)
251
295
 
252
- def charset(str)
253
- m = str.to_s[CHARSET_RE, 1]
254
- m && m.strip.delete('"')
255
- end
296
+ m = @header_value.to_s[CHARSET_RE, 1]
297
+ m && @charset = m.strip.delete('"')
256
298
  end
257
299
  end
258
300
 
@@ -286,14 +328,6 @@ module HTTPX
286
328
  def raise_for_status
287
329
  raise @error
288
330
  end
289
-
290
- # rubocop:disable Style/MissingRespondToMissing
291
- def method_missing(meth, *, &block)
292
- raise NoMethodError, "undefined response method `#{meth}' for error response" if @options.response_class.public_method_defined?(meth)
293
-
294
- super
295
- end
296
- # rubocop:enable Style/MissingRespondToMissing
297
331
  end
298
332
  end
299
333