httpx 0.20.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (250) 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 +5 -5
  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_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -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
@@ -39,6 +39,7 @@ module HTTPX
39
39
  @uri_addresses = nil
40
40
  @resolver = Resolv::DNS.new
41
41
  @resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
42
+ @resolver.lazy_initialize
42
43
  end
43
44
 
44
45
  def <<(connection)
@@ -49,6 +50,7 @@ module HTTPX
49
50
  if @uri_addresses.empty?
50
51
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
51
52
  ex.set_backtrace(caller)
53
+ connection.force_reset
52
54
  throw(:resolve_error, ex)
53
55
  end
54
56
 
@@ -66,11 +68,14 @@ module HTTPX
66
68
  def resolver_connection
67
69
  @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
68
70
  @building_connection = true
69
- 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] }))
70
72
  @pool.init_connection(connection, @options)
71
- emit_addresses(connection, @family, @uri_addresses)
72
- @building_connection = false
73
- connection
73
+ # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
74
+ catch(:coalesced) do
75
+ @building_connection = false
76
+ emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
77
+ connection
78
+ end
74
79
  end
75
80
  end
76
81
 
@@ -101,8 +106,8 @@ module HTTPX
101
106
  @requests[request] = hostname
102
107
  resolver_connection.send(request)
103
108
  @connections << connection
104
- rescue ResolveError, Resolv::DNS::EncodeError, JSON::JSONError => e
105
- @queries.delete(hostname)
109
+ rescue ResolveError, Resolv::DNS::EncodeError => e
110
+ reset_hostname(hostname)
106
111
  emit_resolve_error(connection, connection.origin.host, e)
107
112
  end
108
113
  end
@@ -111,7 +116,7 @@ module HTTPX
111
116
  response.raise_for_status
112
117
  rescue StandardError => e
113
118
  hostname = @requests.delete(request)
114
- connection = @queries.delete(hostname)
119
+ connection = reset_hostname(hostname)
115
120
  emit_resolve_error(connection, connection.origin.host, e)
116
121
  else
117
122
  # @type var response: HTTPX::Response
@@ -126,17 +131,40 @@ module HTTPX
126
131
  end
127
132
 
128
133
  def parse(request, response)
129
- begin
130
- answers = decode_response_body(response)
131
- rescue Resolv::DNS::DecodeError, JSON::JSONError => e
132
- host, connection = @queries.first
133
- @queries.delete(host)
134
- emit_resolve_error(connection, connection.origin.host, e)
135
- return
134
+ code, result = decode_response_body(response)
135
+
136
+ case code
137
+ when :ok
138
+ parse_addresses(result, request)
139
+ when :no_domain_found
140
+ # Indicates no such domain was found.
141
+
142
+ host = @requests.delete(request)
143
+ connection = reset_hostname(host, reset_candidates: false)
144
+
145
+ unless @queries.value?(connection)
146
+ emit_resolve_error(connection)
147
+ return
148
+ end
149
+
150
+ resolve
151
+ when :dns_error
152
+ host = @requests.delete(request)
153
+ connection = reset_hostname(host)
154
+
155
+ emit_resolve_error(connection)
156
+ when :decode_error
157
+ host = @requests.delete(request)
158
+ connection = reset_hostname(host)
159
+ emit_resolve_error(connection, connection.origin.host, result)
136
160
  end
137
- if answers.nil? || answers.empty?
161
+ end
162
+
163
+ def parse_addresses(answers, request)
164
+ if answers.empty?
165
+ # no address found, eliminate candidates
138
166
  host = @requests.delete(request)
139
- connection = @queries.delete(host)
167
+ connection = reset_hostname(host)
140
168
  emit_resolve_error(connection)
141
169
  return
142
170
 
@@ -147,7 +175,7 @@ module HTTPX
147
175
  if address.key?("alias")
148
176
  alias_address = answers[address["alias"]]
149
177
  if alias_address.nil?
150
- @queries.delete(address["name"])
178
+ reset_hostname(address["name"])
151
179
  if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
152
180
  @connections.delete(connection)
153
181
  else
@@ -164,7 +192,7 @@ module HTTPX
164
192
  next if addresses.empty?
165
193
 
166
194
  hostname.delete_suffix!(".") if hostname.end_with?(".")
167
- connection = @queries.delete(hostname)
195
+ connection = reset_hostname(hostname, reset_candidates: false)
168
196
  next unless connection # probably a retried query for which there's an answer
169
197
 
170
198
  @connections.delete(connection)
@@ -173,7 +201,7 @@ module HTTPX
173
201
  @queries.delete_if { |_, conn| connection == conn }
174
202
 
175
203
  Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
176
- emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
204
+ catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
177
205
  end
178
206
  end
179
207
  return if @connections.empty?
@@ -193,7 +221,7 @@ module HTTPX
193
221
  uri.query = URI.encode_www_form(params)
194
222
  request = rklass.new("GET", uri, @options)
195
223
  else
196
- request = rklass.new("POST", uri, @options.merge(body: [payload]))
224
+ request = rklass.new("POST", uri, @options, body: [payload])
197
225
  request.headers["content-type"] = "application/dns-message"
198
226
  end
199
227
  request.headers["accept"] = "application/dns-message"
@@ -202,11 +230,6 @@ module HTTPX
202
230
 
203
231
  def decode_response_body(response)
204
232
  case response.headers["content-type"]
205
- when "application/dns-json",
206
- "application/json",
207
- %r{^application/x-javascript} # because google...
208
- payload = JSON.parse(response.to_s)
209
- payload["Answer"]
210
233
  when "application/dns-udpwireformat",
211
234
  "application/dns-message"
212
235
  Resolver.decode_dns_answer(response.to_s)
@@ -214,5 +237,17 @@ module HTTPX
214
237
  raise Error, "unsupported DNS mime-type (#{response.headers["content-type"]})"
215
238
  end
216
239
  end
240
+
241
+ def reset_hostname(hostname, reset_candidates: true)
242
+ connection = @queries.delete(hostname)
243
+
244
+ return connection unless connection && reset_candidates
245
+
246
+ # eliminate other candidates
247
+ candidates = @queries.select { |_, conn| connection == conn }.keys
248
+ @queries.delete_if { |h, _| candidates.include?(h) }
249
+
250
+ connection
251
+ end
217
252
  end
218
253
  end
@@ -6,7 +6,7 @@ require "resolv"
6
6
  module HTTPX
7
7
  class Resolver::Multi
8
8
  include Callbacks
9
- using ArrayExtensions
9
+ using ArrayExtensions::FilterMap
10
10
 
11
11
  attr_reader :resolvers
12
12
 
@@ -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)
@@ -8,32 +8,12 @@ module HTTPX
8
8
  extend Forwardable
9
9
  using URIExtensions
10
10
 
11
- DEFAULTS = if RUBY_VERSION < "2.2"
12
- {
13
- **Resolv::DNS::Config.default_config_hash,
14
- packet_size: 512,
15
- timeouts: Resolver::RESOLVE_TIMEOUT,
16
- }.freeze
17
- else
18
- {
19
- nameserver: nil,
20
- **Resolv::DNS::Config.default_config_hash,
21
- packet_size: 512,
22
- timeouts: Resolver::RESOLVE_TIMEOUT,
23
- }.freeze
24
- end
25
-
26
- # nameservers for ipv6 are misconfigured in certain systems;
27
- # this can use an unexpected endless loop
28
- # https://gitlab.com/honeyryderchuck/httpx/issues/56
29
- DEFAULTS[:nameserver].select! do |nameserver|
30
- begin
31
- IPAddr.new(nameserver)
32
- true
33
- rescue IPAddr::InvalidAddressError
34
- false
35
- end
36
- end if DEFAULTS[:nameserver]
11
+ DEFAULTS = {
12
+ nameserver: nil,
13
+ **Resolv::DNS::Config.default_config_hash,
14
+ packet_size: 512,
15
+ timeouts: Resolver::RESOLVE_TIMEOUT,
16
+ }.freeze
37
17
 
38
18
  DNS_PORT = 53
39
19
 
@@ -41,12 +21,16 @@ module HTTPX
41
21
 
42
22
  attr_reader :state
43
23
 
44
- def initialize(_, options)
24
+ def initialize(family, options)
45
25
  super
46
26
  @ns_index = 0
47
27
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
48
- @nameserver = @resolver_options[:nameserver]
49
- @ndots = @resolver_options[:ndots]
28
+ @socket_type = @resolver_options.fetch(:socket_type, :udp)
29
+ @nameserver = if (nameserver = @resolver_options[:nameserver])
30
+ nameserver = nameserver[family] if nameserver.is_a?(Hash)
31
+ Array(nameserver)
32
+ end
33
+ @ndots = @resolver_options.fetch(:ndots, 1)
50
34
  @search = Array(@resolver_options[:search]).map { |srch| srch.scan(/[^.]+/) }
51
35
  @_timeouts = Array(@resolver_options[:timeouts])
52
36
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
@@ -77,9 +61,11 @@ module HTTPX
77
61
  nil
78
62
  rescue Errno::EHOSTUNREACH => e
79
63
  @ns_index += 1
80
- if @ns_index < @nameserver.size
64
+ nameserver = @nameserver
65
+ if nameserver && @ns_index < nameserver.size
81
66
  log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
82
67
  transition(:idle)
68
+ @timeouts.clear
83
69
  else
84
70
  handle_error(e)
85
71
  end
@@ -103,6 +89,7 @@ module HTTPX
103
89
  if @nameserver.nil?
104
90
  ex = ResolveError.new("No available nameserver")
105
91
  ex.set_backtrace(caller)
92
+ connection.force_reset
106
93
  throw(:resolve_error, ex)
107
94
  else
108
95
  @connections << connection
@@ -118,6 +105,10 @@ module HTTPX
118
105
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
119
106
  end
120
107
 
108
+ def handle_socket_timeout(interval)
109
+ do_retry(interval)
110
+ end
111
+
121
112
  private
122
113
 
123
114
  def calculate_interests
@@ -134,10 +125,10 @@ module HTTPX
134
125
  dwrite if calculate_interests == :w
135
126
  end
136
127
 
137
- def do_retry
128
+ def do_retry(loop_time = nil)
138
129
  return if @queries.empty? || !@start_timeout
139
130
 
140
- loop_time = Utils.elapsed_time(@start_timeout)
131
+ loop_time ||= Utils.elapsed_time(@start_timeout)
141
132
 
142
133
  query = @queries.first
143
134
 
@@ -147,12 +138,26 @@ module HTTPX
147
138
  host = connection.origin.host
148
139
  timeout = (@timeouts[host][0] -= loop_time)
149
140
 
150
- return unless timeout.negative?
141
+ return unless timeout <= 0
151
142
 
152
143
  @timeouts[host].shift
153
- if @timeouts[host].empty?
144
+
145
+ if !@timeouts[host].empty?
146
+ log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
147
+ # must downgrade to tcp AND retry on same host as last
148
+ downgrade_socket
149
+ resolve(connection, h)
150
+ elsif @ns_index + 1 < @nameserver.size
151
+ # try on the next nameserver
152
+ @ns_index += 1
153
+ log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
154
+ transition(:idle)
155
+ @timeouts.clear
156
+ resolve(connection, h)
157
+ else
158
+
154
159
  @timeouts.delete(host)
155
- @queries.delete(h)
160
+ reset_hostname(h, reset_candidates: false)
156
161
 
157
162
  return unless @queries.empty?
158
163
 
@@ -160,18 +165,54 @@ module HTTPX
160
165
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
161
166
  # resolve timeout, including from the previous retries.
162
167
  raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
163
- else
164
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
165
- resolve(connection)
166
168
  end
167
169
  end
168
170
 
169
171
  def dread(wsize = @resolver_options[:packet_size])
170
172
  loop do
173
+ wsize = @large_packet.capacity if @large_packet
174
+
171
175
  siz = @io.read(wsize, @read_buffer)
172
- return unless siz && siz.positive?
173
176
 
174
- parse(@read_buffer)
177
+ unless siz
178
+ ex = EOFError.new("descriptor closed")
179
+ ex.set_backtrace(caller)
180
+ raise ex
181
+ end
182
+
183
+ return unless siz.positive?
184
+
185
+ if @socket_type == :tcp
186
+ # packet may be incomplete, need to keep draining from the socket
187
+ if @large_packet
188
+ # large packet buffer already exists, continue pumping
189
+ @large_packet << @read_buffer
190
+
191
+ next unless @large_packet.full?
192
+
193
+ parse(@large_packet.to_s)
194
+ @large_packet = nil
195
+ # downgrade to udp again
196
+ downgrade_socket
197
+ return
198
+ else
199
+ size = @read_buffer[0, 2].unpack1("n")
200
+ buffer = @read_buffer.byteslice(2..-1)
201
+
202
+ if size > @read_buffer.bytesize
203
+ # only do buffer logic if it's worth it, and the whole packet isn't here already
204
+ @large_packet = Buffer.new(size)
205
+ @large_packet << buffer
206
+
207
+ next
208
+ else
209
+ parse(buffer)
210
+ end
211
+ end
212
+ else # udp
213
+ parse(@read_buffer)
214
+ end
215
+
175
216
  return if @state == :closed
176
217
  end
177
218
  end
@@ -181,34 +222,68 @@ module HTTPX
181
222
  return if @write_buffer.empty?
182
223
 
183
224
  siz = @io.write(@write_buffer)
184
- return unless siz && siz.positive?
225
+
226
+ unless siz
227
+ ex = EOFError.new("descriptor closed")
228
+ ex.set_backtrace(caller)
229
+ raise ex
230
+ end
231
+
232
+ return unless siz.positive?
185
233
 
186
234
  return if @state == :closed
187
235
  end
188
236
  end
189
237
 
190
238
  def parse(buffer)
191
- begin
192
- addresses = Resolver.decode_dns_answer(buffer)
193
- rescue Resolv::DNS::DecodeError => e
194
- hostname, connection = @queries.first
195
- @queries.delete(hostname)
196
- @timeouts.delete(hostname)
197
- @connections.delete(connection)
198
- ex = NativeResolveError.new(connection, connection.origin.host, e.message)
199
- ex.set_backtrace(e.backtrace)
200
- raise ex
201
- end
239
+ code, result = Resolver.decode_dns_answer(buffer)
202
240
 
203
- if addresses.nil? || addresses.empty?
241
+ case code
242
+ when :ok
243
+ parse_addresses(result)
244
+ when :no_domain_found
245
+ # Indicates no such domain was found.
204
246
  hostname, connection = @queries.first
205
- @queries.delete(hostname)
206
- @timeouts.delete(hostname)
247
+ reset_hostname(hostname, reset_candidates: false)
207
248
 
208
249
  unless @queries.value?(connection)
209
250
  @connections.delete(connection)
210
- raise NativeResolveError.new(connection, connection.origin.host)
251
+ raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
211
252
  end
253
+
254
+ resolve
255
+ when :message_truncated
256
+ # TODO: what to do if it's already tcp??
257
+ return if @socket_type == :tcp
258
+
259
+ @socket_type = :tcp
260
+
261
+ hostname, _ = @queries.first
262
+ reset_hostname(hostname)
263
+ transition(:closed)
264
+ when :dns_error
265
+ hostname, connection = @queries.first
266
+ reset_hostname(hostname)
267
+ @connections.delete(connection)
268
+ ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})")
269
+ raise ex
270
+ when :decode_error
271
+ hostname, connection = @queries.first
272
+ reset_hostname(hostname)
273
+ @connections.delete(connection)
274
+ ex = NativeResolveError.new(connection, connection.origin.host, result.message)
275
+ ex.set_backtrace(result.backtrace)
276
+ raise ex
277
+ end
278
+ end
279
+
280
+ def parse_addresses(addresses)
281
+ if addresses.empty?
282
+ # no address found, eliminate candidates
283
+ hostname, connection = @queries.first
284
+ reset_hostname(hostname)
285
+ @connections.delete(connection)
286
+ raise NativeResolveError.new(connection, connection.origin.host)
212
287
  else
213
288
  address = addresses.first
214
289
  name = address["name"]
@@ -216,36 +291,45 @@ module HTTPX
216
291
  connection = @queries.delete(name)
217
292
 
218
293
  unless connection
294
+ orig_name = name
219
295
  # absolute name
220
296
  name_labels = Resolv::DNS::Name.create(name).to_a
221
- name = @queries.keys.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
297
+ name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
222
298
 
223
299
  # probably a retried query for which there's an answer
224
- return unless name
300
+ unless name
301
+ @timeouts.delete(orig_name)
302
+ return
303
+ end
225
304
 
226
305
  address["name"] = name
227
306
  connection = @queries.delete(name)
228
307
  end
229
308
 
230
- # eliminate other candidates
231
- @queries.delete_if { |_, conn| connection == conn }
232
-
233
309
  if address.key?("alias") # CNAME
310
+ hostname_alias = address["alias"]
234
311
  # clean up intermediate queries
235
312
  @timeouts.delete(name) unless connection.origin.host == name
236
313
 
237
- if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
314
+ if catch(:coalesced) { early_resolve(connection, hostname: hostname_alias) }
238
315
  @connections.delete(connection)
239
316
  else
240
- resolve(connection, address["alias"])
317
+ if @socket_type == :tcp
318
+ # must downgrade to udp if tcp
319
+ @socket_type = @resolver_options.fetch(:socket_type, :udp)
320
+ transition(:idle)
321
+ transition(:open)
322
+ end
323
+ log { "resolver: ALIAS #{hostname_alias} for #{name}" }
324
+ resolve(connection, hostname_alias)
241
325
  return
242
326
  end
243
327
  else
244
- @timeouts.delete(name)
328
+ reset_hostname(name, connection: connection)
245
329
  @timeouts.delete(connection.origin.host)
246
330
  @connections.delete(connection)
247
331
  Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
248
- emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
332
+ catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
249
333
  end
250
334
  end
251
335
  return emit(:close) if @connections.empty?
@@ -255,6 +339,7 @@ module HTTPX
255
339
 
256
340
  def resolve(connection = @connections.first, hostname = nil)
257
341
  raise Error, "no URI to resolve" unless connection
342
+
258
343
  return unless @write_buffer.empty?
259
344
 
260
345
  hostname ||= @queries.key(connection)
@@ -271,12 +356,19 @@ module HTTPX
271
356
  end
272
357
  log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
273
358
  begin
274
- @write_buffer << Resolver.encode_dns_query(hostname, type: @record_type)
359
+ @write_buffer << encode_dns_query(hostname)
275
360
  rescue Resolv::DNS::EncodeError => e
276
361
  emit_resolve_error(connection, hostname, e)
277
362
  end
278
363
  end
279
364
 
365
+ def encode_dns_query(hostname)
366
+ message_id = Resolver.generate_id
367
+ msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
368
+ msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
369
+ msg
370
+ end
371
+
280
372
  def generate_candidates(name)
281
373
  return [name] if name.end_with?(".")
282
374
 
@@ -284,21 +376,33 @@ module HTTPX
284
376
  name_parts = name.scan(/[^.]+/)
285
377
  candidates = [name] if @ndots <= name_parts.size - 1
286
378
  candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") })
287
- candidates << name unless candidates.include?(name)
379
+ fname = "#{name}."
380
+ candidates << fname unless candidates.include?(fname)
288
381
 
289
382
  candidates
290
383
  end
291
384
 
292
385
  def build_socket
293
- return if @io
294
-
295
386
  ip, port = @nameserver[@ns_index]
296
387
  port ||= DNS_PORT
297
- uri = URI::Generic.build(scheme: "udp", port: port)
298
- uri.hostname = ip
299
- type = IO.registry(uri.scheme)
300
- log { "resolver: server: #{uri}..." }
301
- @io = type.new(uri, [IPAddr.new(ip)], @options)
388
+
389
+ case @socket_type
390
+ when :udp
391
+ log { "resolver: server: udp://#{ip}:#{port}..." }
392
+ UDP.new(ip, port, @options)
393
+ when :tcp
394
+ log { "resolver: server: tcp://#{ip}:#{port}..." }
395
+ origin = URI("tcp://#{ip}:#{port}")
396
+ TCP.new(origin, [ip], @options)
397
+ end
398
+ end
399
+
400
+ def downgrade_socket
401
+ return unless @socket_type == :tcp
402
+
403
+ @socket_type = @resolver_options.fetch(:socket_type, :udp)
404
+ transition(:idle)
405
+ transition(:open)
302
406
  end
303
407
 
304
408
  def transition(nextstate)
@@ -308,11 +412,10 @@ module HTTPX
308
412
  @io.close
309
413
  @io = nil
310
414
  end
311
- @timeouts.clear
312
415
  when :open
313
416
  return unless @state == :idle
314
417
 
315
- build_socket
418
+ @io ||= build_socket
316
419
 
317
420
  @io.connect
318
421
  return unless @io.connected?
@@ -339,5 +442,18 @@ module HTTPX
339
442
  end
340
443
  end
341
444
  end
445
+
446
+ def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
447
+ @timeouts.delete(hostname)
448
+ @timeouts.delete(hostname)
449
+
450
+ return unless connection && reset_candidates
451
+
452
+ # eliminate other candidates
453
+ candidates = @queries.select { |_, conn| connection == conn }.keys
454
+ @queries.delete_if { |h, _| candidates.include?(h) }
455
+ # reset timeouts
456
+ @timeouts.delete_if { |h, _| candidates.include?(h) }
457
+ end
342
458
  end
343
459
  end