httpx 0.21.0 → 1.2.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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +4 -4
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_20_0.md +1 -1
  15. data/doc/release_notes/0_21_0.md +7 -5
  16. data/doc/release_notes/0_21_1.md +12 -0
  17. data/doc/release_notes/0_22_0.md +13 -0
  18. data/doc/release_notes/0_22_1.md +11 -0
  19. data/doc/release_notes/0_22_2.md +5 -0
  20. data/doc/release_notes/0_22_3.md +55 -0
  21. data/doc/release_notes/0_22_4.md +6 -0
  22. data/doc/release_notes/0_22_5.md +6 -0
  23. data/doc/release_notes/0_23_0.md +42 -0
  24. data/doc/release_notes/0_23_1.md +5 -0
  25. data/doc/release_notes/0_23_2.md +5 -0
  26. data/doc/release_notes/0_23_3.md +6 -0
  27. data/doc/release_notes/0_23_4.md +5 -0
  28. data/doc/release_notes/0_24_0.md +48 -0
  29. data/doc/release_notes/0_24_1.md +12 -0
  30. data/doc/release_notes/0_24_2.md +12 -0
  31. data/doc/release_notes/0_24_3.md +12 -0
  32. data/doc/release_notes/0_24_4.md +18 -0
  33. data/doc/release_notes/0_24_5.md +6 -0
  34. data/doc/release_notes/0_24_6.md +5 -0
  35. data/doc/release_notes/0_24_7.md +10 -0
  36. data/doc/release_notes/1_0_0.md +60 -0
  37. data/doc/release_notes/1_0_1.md +5 -0
  38. data/doc/release_notes/1_0_2.md +7 -0
  39. data/doc/release_notes/1_1_0.md +32 -0
  40. data/doc/release_notes/1_1_1.md +17 -0
  41. data/doc/release_notes/1_1_2.md +12 -0
  42. data/doc/release_notes/1_1_3.md +18 -0
  43. data/doc/release_notes/1_1_4.md +6 -0
  44. data/doc/release_notes/1_1_5.md +12 -0
  45. data/doc/release_notes/1_2_0.md +49 -0
  46. data/doc/release_notes/1_2_1.md +6 -0
  47. data/lib/httpx/adapters/datadog.rb +100 -106
  48. data/lib/httpx/adapters/faraday.rb +143 -107
  49. data/lib/httpx/adapters/sentry.rb +26 -7
  50. data/lib/httpx/adapters/webmock.rb +33 -17
  51. data/lib/httpx/altsvc.rb +61 -24
  52. data/lib/httpx/base64.rb +27 -0
  53. data/lib/httpx/buffer.rb +12 -0
  54. data/lib/httpx/callbacks.rb +5 -3
  55. data/lib/httpx/chainable.rb +54 -39
  56. data/lib/httpx/connection/http1.rb +62 -37
  57. data/lib/httpx/connection/http2.rb +16 -27
  58. data/lib/httpx/connection.rb +213 -120
  59. data/lib/httpx/domain_name.rb +10 -13
  60. data/lib/httpx/errors.rb +34 -2
  61. data/lib/httpx/extensions.rb +4 -134
  62. data/lib/httpx/io/ssl.rb +77 -71
  63. data/lib/httpx/io/tcp.rb +46 -70
  64. data/lib/httpx/io/udp.rb +18 -52
  65. data/lib/httpx/io/unix.rb +6 -13
  66. data/lib/httpx/io.rb +3 -9
  67. data/lib/httpx/loggable.rb +4 -19
  68. data/lib/httpx/options.rb +168 -110
  69. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  70. data/lib/httpx/plugins/{authentication → auth}/digest.rb +13 -14
  71. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  72. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  73. data/lib/httpx/plugins/auth.rb +25 -0
  74. data/lib/httpx/plugins/aws_sdk_authentication.rb +1 -3
  75. data/lib/httpx/plugins/aws_sigv4.rb +5 -6
  76. data/lib/httpx/plugins/basic_auth.rb +29 -0
  77. data/lib/httpx/plugins/brotli.rb +50 -0
  78. data/lib/httpx/plugins/callbacks.rb +91 -0
  79. data/lib/httpx/plugins/circuit_breaker/circuit.rb +40 -16
  80. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +14 -5
  81. data/lib/httpx/plugins/circuit_breaker.rb +30 -7
  82. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  83. data/lib/httpx/plugins/cookies.rb +20 -10
  84. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +11 -12
  85. data/lib/httpx/plugins/expect.rb +15 -13
  86. data/lib/httpx/plugins/follow_redirects.rb +71 -29
  87. data/lib/httpx/plugins/grpc/call.rb +2 -3
  88. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  89. data/lib/httpx/plugins/grpc/message.rb +7 -37
  90. data/lib/httpx/plugins/grpc.rb +35 -29
  91. data/lib/httpx/plugins/h2c.rb +25 -18
  92. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  93. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  94. data/lib/httpx/plugins/oauth.rb +170 -0
  95. data/lib/httpx/plugins/persistent.rb +1 -1
  96. data/lib/httpx/plugins/proxy/http.rb +15 -10
  97. data/lib/httpx/plugins/proxy/socks4.rb +8 -6
  98. data/lib/httpx/plugins/proxy/socks5.rb +10 -8
  99. data/lib/httpx/plugins/proxy.rb +69 -67
  100. data/lib/httpx/plugins/push_promise.rb +1 -1
  101. data/lib/httpx/plugins/rate_limiter.rb +3 -1
  102. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  103. data/lib/httpx/plugins/response_cache/store.rb +34 -17
  104. data/lib/httpx/plugins/response_cache.rb +6 -6
  105. data/lib/httpx/plugins/retries.rb +61 -12
  106. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  107. data/lib/httpx/plugins/stream.rb +27 -32
  108. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  109. data/lib/httpx/plugins/upgrade.rb +8 -10
  110. data/lib/httpx/plugins/webdav.rb +10 -8
  111. data/lib/httpx/pool.rb +85 -23
  112. data/lib/httpx/punycode.rb +9 -291
  113. data/lib/httpx/request/body.rb +158 -0
  114. data/lib/httpx/request.rb +86 -121
  115. data/lib/httpx/resolver/https.rb +54 -17
  116. data/lib/httpx/resolver/multi.rb +8 -12
  117. data/lib/httpx/resolver/native.rb +163 -70
  118. data/lib/httpx/resolver/resolver.rb +28 -13
  119. data/lib/httpx/resolver/system.rb +15 -10
  120. data/lib/httpx/resolver.rb +38 -16
  121. data/lib/httpx/response/body.rb +242 -0
  122. data/lib/httpx/response/buffer.rb +96 -0
  123. data/lib/httpx/response.rb +113 -211
  124. data/lib/httpx/selector.rb +2 -4
  125. data/lib/httpx/session.rb +91 -64
  126. data/lib/httpx/session_extensions.rb +4 -1
  127. data/lib/httpx/timers.rb +28 -8
  128. data/lib/httpx/transcoder/body.rb +0 -2
  129. data/lib/httpx/transcoder/chunker.rb +0 -1
  130. data/lib/httpx/transcoder/deflate.rb +37 -0
  131. data/lib/httpx/transcoder/form.rb +52 -33
  132. data/lib/httpx/transcoder/gzip.rb +74 -0
  133. data/lib/httpx/transcoder/json.rb +2 -5
  134. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  135. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +3 -3
  136. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  137. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  138. data/lib/httpx/transcoder/multipart.rb +17 -0
  139. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  140. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  141. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  142. data/lib/httpx/transcoder/xml.rb +0 -5
  143. data/lib/httpx/transcoder.rb +4 -6
  144. data/lib/httpx/utils.rb +36 -16
  145. data/lib/httpx/version.rb +1 -1
  146. data/lib/httpx.rb +12 -14
  147. data/sig/altsvc.rbs +33 -0
  148. data/sig/buffer.rbs +1 -0
  149. data/sig/callbacks.rbs +3 -3
  150. data/sig/chainable.rbs +10 -9
  151. data/sig/connection/http1.rbs +5 -4
  152. data/sig/connection/http2.rbs +1 -1
  153. data/sig/connection.rbs +46 -24
  154. data/sig/errors.rbs +9 -3
  155. data/sig/httpx.rbs +5 -4
  156. data/sig/io/ssl.rbs +26 -0
  157. data/sig/io/tcp.rbs +60 -0
  158. data/sig/io/udp.rbs +20 -0
  159. data/sig/io/unix.rbs +10 -0
  160. data/sig/options.rbs +28 -12
  161. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  162. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  163. data/sig/plugins/auth.rbs +13 -0
  164. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  165. data/sig/plugins/brotli.rbs +22 -0
  166. data/sig/plugins/callbacks.rbs +38 -0
  167. data/sig/plugins/circuit_breaker.rbs +13 -3
  168. data/sig/plugins/compression.rbs +6 -4
  169. data/sig/plugins/cookies/jar.rbs +2 -2
  170. data/sig/plugins/cookies.rbs +2 -0
  171. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  172. data/sig/plugins/follow_redirects.rbs +11 -2
  173. data/sig/plugins/grpc/call.rbs +19 -0
  174. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  175. data/sig/plugins/grpc/message.rbs +17 -0
  176. data/sig/plugins/grpc.rbs +2 -32
  177. data/sig/plugins/h2c.rbs +1 -1
  178. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  179. data/sig/plugins/oauth.rbs +54 -0
  180. data/sig/plugins/proxy/socks4.rbs +4 -4
  181. data/sig/plugins/proxy/socks5.rbs +2 -2
  182. data/sig/plugins/proxy/ssh.rbs +1 -1
  183. data/sig/plugins/proxy.rbs +10 -4
  184. data/sig/plugins/response_cache.rbs +12 -3
  185. data/sig/plugins/retries.rbs +28 -8
  186. data/sig/plugins/stream.rbs +24 -17
  187. data/sig/plugins/upgrade.rbs +5 -3
  188. data/sig/pool.rbs +5 -4
  189. data/sig/request/body.rbs +40 -0
  190. data/sig/request.rbs +12 -28
  191. data/sig/resolver/https.rbs +7 -2
  192. data/sig/resolver/native.rbs +10 -4
  193. data/sig/resolver/resolver.rbs +6 -4
  194. data/sig/resolver/system.rbs +2 -0
  195. data/sig/resolver.rbs +9 -5
  196. data/sig/response/body.rbs +53 -0
  197. data/sig/response/buffer.rbs +24 -0
  198. data/sig/response.rbs +17 -38
  199. data/sig/session.rbs +24 -18
  200. data/sig/timers.rbs +17 -7
  201. data/sig/transcoder/body.rbs +4 -3
  202. data/sig/transcoder/deflate.rbs +11 -0
  203. data/sig/transcoder/form.rbs +5 -3
  204. data/sig/transcoder/gzip.rbs +24 -0
  205. data/sig/transcoder/json.rbs +4 -2
  206. data/sig/{plugins → transcoder}/multipart.rbs +3 -12
  207. data/sig/transcoder/utils/body_reader.rbs +15 -0
  208. data/sig/transcoder/utils/deflater.rbs +29 -0
  209. data/sig/transcoder/utils/inflater.rbs +12 -0
  210. data/sig/transcoder/xml.rbs +1 -1
  211. data/sig/transcoder.rbs +22 -7
  212. data/sig/utils.rbs +2 -0
  213. metadata +127 -40
  214. data/lib/httpx/plugins/authentication.rb +0 -20
  215. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  216. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  217. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  218. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  219. data/lib/httpx/plugins/compression.rb +0 -164
  220. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  221. data/lib/httpx/plugins/multipart.rb +0 -84
  222. data/lib/httpx/registry.rb +0 -85
  223. data/sig/plugins/authentication.rbs +0 -11
  224. data/sig/plugins/compression/brotli.rbs +0 -21
  225. data/sig/plugins/compression/deflate.rbs +0 -17
  226. data/sig/plugins/compression/gzip.rbs +0 -29
  227. data/sig/registry.rbs +0 -13
  228. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  229. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ # Implementation of the HTTP Request body as a delegator which iterates (responds to +each+) payload chunks.
5
+ class Request::Body < SimpleDelegator
6
+ class << self
7
+ def new(_, options)
8
+ return options.body if options.body.is_a?(self)
9
+
10
+ super
11
+ end
12
+ end
13
+
14
+ # inits the instance with the request +headers+ and +options+, which contain the payload definition.
15
+ def initialize(headers, options)
16
+ @headers = headers
17
+
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
23
+ end
24
+
25
+ initialize_body(options)
26
+
27
+ return if @body.nil?
28
+
29
+ @headers["content-type"] ||= @body.content_type
30
+ @headers["content-length"] = @body.bytesize unless unbounded_body?
31
+ super(@body)
32
+ end
33
+
34
+ # consumes and yields the request payload in chunks.
35
+ def each(&block)
36
+ return enum_for(__method__) unless block
37
+ return if @body.nil?
38
+
39
+ body = stream(@body)
40
+ if body.respond_to?(:read)
41
+ ::IO.copy_stream(body, ProcIO.new(block))
42
+ elsif body.respond_to?(:each)
43
+ body.each(&block)
44
+ else
45
+ block[body.to_s]
46
+ end
47
+ end
48
+
49
+ # if the +@body+ is rewindable, it rewinnds it.
50
+ def rewind
51
+ return if empty?
52
+
53
+ @body.rewind if @body.respond_to?(:rewind)
54
+ end
55
+
56
+ # return +true+ if the +body+ has been fully drained (or does nnot exist).
57
+ def empty?
58
+ return true if @body.nil?
59
+ return false if chunked?
60
+
61
+ @body.bytesize.zero?
62
+ end
63
+
64
+ # returns the +@body+ payload size in bytes.
65
+ def bytesize
66
+ return 0 if @body.nil?
67
+
68
+ @body.bytesize
69
+ end
70
+
71
+ # sets the body to yield using chunked trannsfer encoding format.
72
+ def stream(body)
73
+ return body unless chunked?
74
+
75
+ Transcoder::Chunker.encode(body.enum_for(:each))
76
+ end
77
+
78
+ # returns whether the body yields infinitely.
79
+ def unbounded_body?
80
+ return @unbounded_body if defined?(@unbounded_body)
81
+
82
+ @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
83
+ end
84
+
85
+ # returns whether the chunked transfer encoding header is set.
86
+ def chunked?
87
+ @headers["transfer-encoding"] == "chunked"
88
+ end
89
+
90
+ # sets the chunked transfer encoding header.
91
+ def chunk!
92
+ @headers.add("transfer-encoding", "chunked")
93
+ end
94
+
95
+ # :nocov:
96
+ def inspect
97
+ "#<HTTPX::Request::Body:#{object_id} " \
98
+ "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
99
+ end
100
+ # :nocov:
101
+
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
+ class << self
130
+ # returns the +body+ wrapped with the correct deflater accordinng to the given +encodisng+.
131
+ def initialize_deflater_body(body, encoding)
132
+ case encoding
133
+ when "gzip"
134
+ Transcoder::GZIP.encode(body)
135
+ when "deflate"
136
+ Transcoder::Deflate.encode(body)
137
+ when "identity"
138
+ body
139
+ else
140
+ body
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ # Wrapper yielder which can be used with functions which expect an IO writer.
147
+ class ProcIO
148
+ def initialize(block)
149
+ @block = block
150
+ end
151
+
152
+ # Implementation the IO write protocol, which yield the given chunk to +@block+.
153
+ def write(data)
154
+ @block.call(data.dup)
155
+ data.bytesize
156
+ end
157
+ end
158
+ end
data/lib/httpx/request.rb CHANGED
@@ -4,22 +4,52 @@ require "delegate"
4
4
  require "forwardable"
5
5
 
6
6
  module HTTPX
7
+ # Defines how an HTTP request is handled internally, both in terms of making attributes accessible,
8
+ # as well as maintaining the state machine which manages streaming the request onto the wire.
7
9
  class Request
8
10
  extend Forwardable
9
11
  include Callbacks
10
12
  using URIExtensions
11
13
 
14
+ # default value used for "user-agent" header, when not overridden.
12
15
  USER_AGENT = "httpx.rb/#{VERSION}"
13
16
 
14
- attr_reader :verb, :uri, :headers, :body, :state, :options, :response
17
+ # the upcased string HTTP verb for this request.
18
+ attr_reader :verb
15
19
 
16
- # Exception raised during enumerable body writes
20
+ # the absolute URI object for this request.
21
+ attr_reader :uri
22
+
23
+ # an HTTPX::Headers object containing the request HTTP headers.
24
+ attr_reader :headers
25
+
26
+ # an HTTPX::Request::Body object containing the request body payload (or +nil+, whenn there is none).
27
+ attr_reader :body
28
+
29
+ # a symbol describing which frame is currently being flushed.
30
+ attr_reader :state
31
+
32
+ # an HTTPX::Options object containing request options.
33
+ attr_reader :options
34
+
35
+ # the corresponding HTTPX::Response object, when there is one.
36
+ attr_reader :response
37
+
38
+ # Exception raised during enumerable body writes.
17
39
  attr_reader :drain_error
18
40
 
41
+ # The IP address from the peer server.
42
+ attr_accessor :peer_address
43
+
44
+ attr_writer :persistent
45
+
46
+ # will be +true+ when request body has been completely flushed.
19
47
  def_delegator :@body, :empty?
20
48
 
49
+ # initializes the instance with the given +verb+, an absolute or relative +uri+, and the
50
+ # request options.
21
51
  def initialize(verb, uri, options = {})
22
- @verb = verb.to_s.downcase.to_sym
52
+ @verb = verb.to_s.upcase
23
53
  @options = Options.new(options)
24
54
  @uri = Utils.to_uri(uri)
25
55
  if @uri.relative?
@@ -37,20 +67,30 @@ module HTTPX
37
67
 
38
68
  @body = @options.request_body_class.new(@headers, @options)
39
69
  @state = :idle
70
+ @response = nil
71
+ @peer_address = nil
72
+ @persistent = @options.persistent
40
73
  end
41
74
 
75
+ # the read timeout defied for this requet.
42
76
  def read_timeout
43
77
  @options.timeout[:read_timeout]
44
78
  end
45
79
 
80
+ # the write timeout defied for this requet.
46
81
  def write_timeout
47
82
  @options.timeout[:write_timeout]
48
83
  end
49
84
 
85
+ # the request timeout defied for this requet.
50
86
  def request_timeout
51
87
  @options.timeout[:request_timeout]
52
88
  end
53
89
 
90
+ def persistent?
91
+ @persistent
92
+ end
93
+
54
94
  def trailers?
55
95
  defined?(@trailers)
56
96
  end
@@ -59,40 +99,45 @@ module HTTPX
59
99
  @trailers ||= @options.headers_class.new
60
100
  end
61
101
 
102
+ # returns +:r+ or +:w+, depending on whether the request is waiting for a response or flushing.
62
103
  def interests
63
104
  return :r if @state == :done || @state == :expect
64
105
 
65
106
  :w
66
107
  end
67
108
 
68
- if RUBY_VERSION < "2.2"
69
- URIParser = URI::DEFAULT_PARSER
70
-
71
- def initialize_with_escape(verb, uri, options = {})
72
- initialize_without_escape(verb, URIParser.escape(uri.to_s), options)
73
- end
74
- alias_method :initialize_without_escape, :initialize
75
- alias_method :initialize, :initialize_with_escape
76
- end
77
-
78
109
  def merge_headers(h)
79
110
  @headers = @headers.merge(h)
80
111
  end
81
112
 
113
+ # the URI scheme of the request +uri+.
82
114
  def scheme
83
115
  @uri.scheme
84
116
  end
85
117
 
118
+ # sets the +response+ on this request.
86
119
  def response=(response)
87
120
  return unless response
88
121
 
89
- if response.is_a?(Response) && response.status == 100
90
- @informational_status = response.status
91
- return
122
+ if response.is_a?(Response) && response.status < 200
123
+ # deal with informational responses
124
+
125
+ if response.status == 100 && @headers.key?("expect")
126
+ @informational_status = response.status
127
+ return
128
+ end
129
+
130
+ # 103 Early Hints advertises resources in document to browsers.
131
+ # not very relevant for an HTTP client, discard.
132
+ return if response.status >= 103
92
133
  end
134
+
93
135
  @response = response
136
+
137
+ emit(:response_started, response)
94
138
  end
95
139
 
140
+ # returnns the URI path of the request +uri+.
96
141
  def path
97
142
  path = uri.path.dup
98
143
  path = +"" if path.nil?
@@ -101,33 +146,48 @@ module HTTPX
101
146
  path
102
147
  end
103
148
 
104
- # https://bugs.ruby-lang.org/issues/15278
149
+ # returs the URI authority of the request.
150
+ #
151
+ # session.build_request("GET", "https://google.com/query").authority #=> "google.com"
152
+ # session.build_request("GET", "http://internal:3182/a").authority #=> "internal:3182"
105
153
  def authority
106
154
  @uri.authority
107
155
  end
108
156
 
109
- # https://bugs.ruby-lang.org/issues/15278
157
+ # returs the URI origin of the request.
158
+ #
159
+ # session.build_request("GET", "https://google.com/query").authority #=> "https://google.com"
160
+ # session.build_request("GET", "http://internal:3182/a").authority #=> "http://internal:3182"
110
161
  def origin
111
162
  @uri.origin
112
163
  end
113
164
 
165
+ # returs the URI query string of the request (when available).
166
+ #
167
+ # session.build_request("GET", "https://search.com").query #=> ""
168
+ # session.build_request("GET", "https://search.com?q=a").query #=> "q=a"
169
+ # session.build_request("GET", "https://search.com", params: { q: "a"}).query #=> "q=a"
170
+ # session.build_request("GET", "https://search.com?q=a", params: { foo: "bar"}).query #=> "q=a&foo&bar"
114
171
  def query
115
172
  return @query if defined?(@query)
116
173
 
117
174
  query = []
118
175
  if (q = @options.params)
119
- query << Transcoder.registry("form").encode(q)
176
+ query << Transcoder::Form.encode(q)
120
177
  end
121
178
  query << @uri.query if @uri.query
122
179
  @query = query.join("&")
123
180
  end
124
181
 
182
+ # consumes and returns the next available chunk of request body that can be sent
125
183
  def drain_body
126
184
  return nil if @body.nil?
127
185
 
128
186
  @drainer ||= @body.each
129
- chunk = @drainer.next
130
- chunk.dup
187
+ chunk = @drainer.next.dup
188
+
189
+ emit(:body_chunk, chunk)
190
+ chunk
131
191
  rescue StopIteration
132
192
  nil
133
193
  rescue StandardError => e
@@ -138,101 +198,14 @@ module HTTPX
138
198
  # :nocov:
139
199
  def inspect
140
200
  "#<HTTPX::Request:#{object_id} " \
141
- "#{@verb.to_s.upcase} " \
201
+ "#{@verb} " \
142
202
  "#{uri} " \
143
203
  "@headers=#{@headers} " \
144
204
  "@body=#{@body}>"
145
205
  end
146
206
  # :nocov:
147
207
 
148
- class Body < SimpleDelegator
149
- class << self
150
- def new(_, options)
151
- return options.body if options.body.is_a?(self)
152
-
153
- super
154
- end
155
- end
156
-
157
- def initialize(headers, options)
158
- @headers = headers
159
- @body = if options.body
160
- Transcoder.registry("body").encode(options.body)
161
- elsif options.form
162
- Transcoder.registry("form").encode(options.form)
163
- elsif options.json
164
- Transcoder.registry("json").encode(options.json)
165
- elsif options.xml
166
- Transcoder.registry("xml").encode(options.xml)
167
- end
168
- return if @body.nil?
169
-
170
- @headers["content-type"] ||= @body.content_type
171
- @headers["content-length"] = @body.bytesize unless unbounded_body?
172
- super(@body)
173
- end
174
-
175
- def each(&block)
176
- return enum_for(__method__) unless block
177
- return if @body.nil?
178
-
179
- body = stream(@body)
180
- if body.respond_to?(:read)
181
- ::IO.copy_stream(body, ProcIO.new(block))
182
- elsif body.respond_to?(:each)
183
- body.each(&block)
184
- else
185
- block[body.to_s]
186
- end
187
- end
188
-
189
- def rewind
190
- return if empty?
191
-
192
- @body.rewind if @body.respond_to?(:rewind)
193
- end
194
-
195
- def empty?
196
- return true if @body.nil?
197
- return false if chunked?
198
-
199
- @body.bytesize.zero?
200
- end
201
-
202
- def bytesize
203
- return 0 if @body.nil?
204
-
205
- @body.bytesize
206
- end
207
-
208
- def stream(body)
209
- encoded = body
210
- encoded = Transcoder.registry("chunker").encode(body.enum_for(:each)) if chunked?
211
- encoded
212
- end
213
-
214
- def unbounded_body?
215
- return @unbounded_body if defined?(@unbounded_body)
216
-
217
- @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
218
- end
219
-
220
- def chunked?
221
- @headers["transfer-encoding"] == "chunked"
222
- end
223
-
224
- def chunk!
225
- @headers.add("transfer-encoding", "chunked")
226
- end
227
-
228
- # :nocov:
229
- def inspect
230
- "#<HTTPX::Request::Body:#{object_id} " \
231
- "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
232
- end
233
- # :nocov:
234
- end
235
-
208
+ # moves on to the +nextstate+ of the request state machine (when all preconditions are met)
236
209
  def transition(nextstate)
237
210
  case nextstate
238
211
  when :idle
@@ -267,19 +240,11 @@ module HTTPX
267
240
  nil
268
241
  end
269
242
 
243
+ # whether the request supports the 100-continue handshake and already processed the 100 response.
270
244
  def expects?
271
245
  @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
272
246
  end
273
-
274
- class ProcIO
275
- def initialize(block)
276
- @block = block
277
- end
278
-
279
- def write(data)
280
- @block.call(data.dup)
281
- data.bytesize
282
- end
283
- end
284
247
  end
285
248
  end
249
+
250
+ require_relative "request/body"
@@ -4,12 +4,12 @@ require "resolv"
4
4
  require "uri"
5
5
  require "cgi"
6
6
  require "forwardable"
7
+ require "httpx/base64"
7
8
 
8
9
  module HTTPX
9
10
  class Resolver::HTTPS < Resolver::Resolver
10
11
  extend Forwardable
11
12
  using URIExtensions
12
- using StringExtensions
13
13
 
14
14
  module DNSExtensions
15
15
  refine Resolv::DNS do
@@ -27,7 +27,7 @@ module HTTPX
27
27
  use_get: false,
28
28
  }.freeze
29
29
 
30
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
30
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
31
31
 
32
32
  def initialize(_, options)
33
33
  super
@@ -50,6 +50,7 @@ module HTTPX
50
50
  if @uri_addresses.empty?
51
51
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
52
52
  ex.set_backtrace(caller)
53
+ connection.force_reset
53
54
  throw(:resolve_error, ex)
54
55
  end
55
56
 
@@ -67,9 +68,10 @@ module HTTPX
67
68
  def resolver_connection
68
69
  @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
69
70
  @building_connection = true
70
- connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
71
+ connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
71
72
  @pool.init_connection(connection, @options)
72
- emit_addresses(connection, @family, @uri_addresses)
73
+ # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
74
+ emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
73
75
  @building_connection = false
74
76
  connection
75
77
  end
@@ -103,7 +105,7 @@ module HTTPX
103
105
  resolver_connection.send(request)
104
106
  @connections << connection
105
107
  rescue ResolveError, Resolv::DNS::EncodeError => e
106
- @queries.delete(hostname)
108
+ reset_hostname(hostname)
107
109
  emit_resolve_error(connection, connection.origin.host, e)
108
110
  end
109
111
  end
@@ -112,7 +114,7 @@ module HTTPX
112
114
  response.raise_for_status
113
115
  rescue StandardError => e
114
116
  hostname = @requests.delete(request)
115
- connection = @queries.delete(hostname)
117
+ connection = reset_hostname(hostname)
116
118
  emit_resolve_error(connection, connection.origin.host, e)
117
119
  else
118
120
  # @type var response: HTTPX::Response
@@ -127,17 +129,40 @@ module HTTPX
127
129
  end
128
130
 
129
131
  def parse(request, response)
130
- begin
131
- answers = decode_response_body(response)
132
- rescue Resolv::DNS::DecodeError => e
133
- host, connection = @queries.first
134
- @queries.delete(host)
135
- emit_resolve_error(connection, connection.origin.host, e)
136
- return
132
+ code, result = decode_response_body(response)
133
+
134
+ case code
135
+ when :ok
136
+ parse_addresses(result, request)
137
+ when :no_domain_found
138
+ # Indicates no such domain was found.
139
+
140
+ host = @requests.delete(request)
141
+ connection = reset_hostname(host, reset_candidates: false)
142
+
143
+ unless @queries.value?(connection)
144
+ emit_resolve_error(connection)
145
+ return
146
+ end
147
+
148
+ resolve
149
+ when :dns_error
150
+ host = @requests.delete(request)
151
+ connection = reset_hostname(host)
152
+
153
+ emit_resolve_error(connection)
154
+ when :decode_error
155
+ host = @requests.delete(request)
156
+ connection = reset_hostname(host)
157
+ emit_resolve_error(connection, connection.origin.host, result)
137
158
  end
138
- if answers.nil? || answers.empty?
159
+ end
160
+
161
+ def parse_addresses(answers, request)
162
+ if answers.empty?
163
+ # no address found, eliminate candidates
139
164
  host = @requests.delete(request)
140
- connection = @queries.delete(host)
165
+ connection = reset_hostname(host)
141
166
  emit_resolve_error(connection)
142
167
  return
143
168
 
@@ -148,7 +173,7 @@ module HTTPX
148
173
  if address.key?("alias")
149
174
  alias_address = answers[address["alias"]]
150
175
  if alias_address.nil?
151
- @queries.delete(address["name"])
176
+ reset_hostname(address["name"])
152
177
  if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
153
178
  @connections.delete(connection)
154
179
  else
@@ -165,7 +190,7 @@ module HTTPX
165
190
  next if addresses.empty?
166
191
 
167
192
  hostname.delete_suffix!(".") if hostname.end_with?(".")
168
- connection = @queries.delete(hostname)
193
+ connection = reset_hostname(hostname, reset_candidates: false)
169
194
  next unless connection # probably a retried query for which there's an answer
170
195
 
171
196
  @connections.delete(connection)
@@ -210,5 +235,17 @@ module HTTPX
210
235
  raise Error, "unsupported DNS mime-type (#{response.headers["content-type"]})"
211
236
  end
212
237
  end
238
+
239
+ def reset_hostname(hostname, reset_candidates: true)
240
+ connection = @queries.delete(hostname)
241
+
242
+ return connection unless connection && reset_candidates
243
+
244
+ # eliminate other candidates
245
+ candidates = @queries.select { |_, conn| connection == conn }.keys
246
+ @queries.delete_if { |h, _| candidates.include?(h) }
247
+
248
+ connection
249
+ end
213
250
  end
214
251
  end
@@ -46,14 +46,15 @@ module HTTPX
46
46
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
47
  return unless addresses
48
48
 
49
- addresses = addresses.group_by(&:family)
49
+ addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
50
+ # try to match the resolver by family. However, there are cases where that's not possible, as when
51
+ # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
52
+ resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
50
53
 
51
- @resolvers.each do |resolver|
52
- addrs = addresses[resolver.family]
54
+ next unless resolver # this should ever happen
53
55
 
54
- next if !addrs || addrs.empty?
55
-
56
- resolver.emit_addresses(connection, resolver.family, addrs)
56
+ # it does not matter which resolver it is, as early-resolve code is shared.
57
+ resolver.emit_addresses(connection, family, addrs, true)
57
58
  end
58
59
  end
59
60
 
@@ -64,12 +65,7 @@ module HTTPX
64
65
  end
65
66
 
66
67
  def on_resolver_error(connection, error)
67
- @errors[connection] << error
68
-
69
- return unless @errors[connection].size >= @resolvers.size
70
-
71
- errors = @errors.delete(connection)
72
- emit(:error, connection, errors.first)
68
+ emit(:error, connection, error)
73
69
  end
74
70
 
75
71
  def on_resolver_close(resolver)