httpx 0.20.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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