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
@@ -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 = Array(@resolver_options[:nameserver]) if @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 }
@@ -104,6 +88,7 @@ module HTTPX
104
88
  if @nameserver.nil?
105
89
  ex = ResolveError.new("No available nameserver")
106
90
  ex.set_backtrace(caller)
91
+ connection.force_reset
107
92
  throw(:resolve_error, ex)
108
93
  else
109
94
  @connections << connection
@@ -119,7 +104,7 @@ module HTTPX
119
104
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
120
105
  end
121
106
 
122
- def raise_timeout_error(interval)
107
+ def handle_socket_timeout(interval)
123
108
  do_retry(interval)
124
109
  end
125
110
 
@@ -152,12 +137,23 @@ module HTTPX
152
137
  host = connection.origin.host
153
138
  timeout = (@timeouts[host][0] -= loop_time)
154
139
 
155
- return unless timeout.negative?
140
+ return unless timeout <= 0
156
141
 
157
142
  @timeouts[host].shift
158
- if @timeouts[host].empty?
143
+
144
+ if !@timeouts[host].empty?
145
+ log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
146
+ resolve(connection)
147
+ elsif @ns_index + 1 < @nameserver.size
148
+ # try on the next nameserver
149
+ @ns_index += 1
150
+ log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
151
+ transition(:idle)
152
+ resolve(connection)
153
+ else
154
+
159
155
  @timeouts.delete(host)
160
- @queries.delete(h)
156
+ reset_hostname(h, reset_candidates: false)
161
157
 
162
158
  return unless @queries.empty?
163
159
 
@@ -165,18 +161,55 @@ module HTTPX
165
161
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
166
162
  # resolve timeout, including from the previous retries.
167
163
  raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
168
- else
169
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
170
- resolve(connection)
171
164
  end
172
165
  end
173
166
 
174
167
  def dread(wsize = @resolver_options[:packet_size])
175
168
  loop do
169
+ wsize = @large_packet.capacity if @large_packet
170
+
176
171
  siz = @io.read(wsize, @read_buffer)
177
- return unless siz && siz.positive?
178
172
 
179
- parse(@read_buffer)
173
+ unless siz
174
+ ex = EOFError.new("descriptor closed")
175
+ ex.set_backtrace(caller)
176
+ raise ex
177
+ end
178
+
179
+ return unless siz.positive?
180
+
181
+ if @socket_type == :tcp
182
+ # packet may be incomplete, need to keep draining from the socket
183
+ if @large_packet
184
+ # large packet buffer already exists, continue pumping
185
+ @large_packet << @read_buffer
186
+
187
+ next unless @large_packet.full?
188
+
189
+ parse(@large_packet.to_s)
190
+
191
+ @socket_type = @resolver_options.fetch(:socket_type, :udp)
192
+ @large_packet = nil
193
+ transition(:closed)
194
+ return
195
+ else
196
+ size = @read_buffer[0, 2].unpack1("n")
197
+ buffer = @read_buffer.byteslice(2..-1)
198
+
199
+ if size > @read_buffer.bytesize
200
+ # only do buffer logic if it's worth it, and the whole packet isn't here already
201
+ @large_packet = Buffer.new(size)
202
+ @large_packet << buffer
203
+
204
+ next
205
+ else
206
+ parse(buffer)
207
+ end
208
+ end
209
+ else # udp
210
+ parse(@read_buffer)
211
+ end
212
+
180
213
  return if @state == :closed
181
214
  end
182
215
  end
@@ -186,34 +219,68 @@ module HTTPX
186
219
  return if @write_buffer.empty?
187
220
 
188
221
  siz = @io.write(@write_buffer)
189
- return unless siz && siz.positive?
222
+
223
+ unless siz
224
+ ex = EOFError.new("descriptor closed")
225
+ ex.set_backtrace(caller)
226
+ raise ex
227
+ end
228
+
229
+ return unless siz.positive?
190
230
 
191
231
  return if @state == :closed
192
232
  end
193
233
  end
194
234
 
195
235
  def parse(buffer)
196
- begin
197
- addresses = Resolver.decode_dns_answer(buffer)
198
- rescue Resolv::DNS::DecodeError => e
199
- hostname, connection = @queries.first
200
- @queries.delete(hostname)
201
- @timeouts.delete(hostname)
202
- @connections.delete(connection)
203
- ex = NativeResolveError.new(connection, connection.origin.host, e.message)
204
- ex.set_backtrace(e.backtrace)
205
- raise ex
206
- end
236
+ code, result = Resolver.decode_dns_answer(buffer)
207
237
 
208
- if addresses.nil? || addresses.empty?
238
+ case code
239
+ when :ok
240
+ parse_addresses(result)
241
+ when :no_domain_found
242
+ # Indicates no such domain was found.
209
243
  hostname, connection = @queries.first
210
- @queries.delete(hostname)
211
- @timeouts.delete(hostname)
244
+ reset_hostname(hostname, reset_candidates: false)
212
245
 
213
246
  unless @queries.value?(connection)
214
247
  @connections.delete(connection)
215
- raise NativeResolveError.new(connection, connection.origin.host)
248
+ raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
216
249
  end
250
+
251
+ resolve
252
+ when :message_truncated
253
+ # TODO: what to do if it's already tcp??
254
+ return if @socket_type == :tcp
255
+
256
+ @socket_type = :tcp
257
+
258
+ hostname, _ = @queries.first
259
+ reset_hostname(hostname)
260
+ transition(:closed)
261
+ when :dns_error
262
+ hostname, connection = @queries.first
263
+ reset_hostname(hostname)
264
+ @connections.delete(connection)
265
+ ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})")
266
+ raise ex
267
+ when :decode_error
268
+ hostname, connection = @queries.first
269
+ reset_hostname(hostname)
270
+ @connections.delete(connection)
271
+ ex = NativeResolveError.new(connection, connection.origin.host, result.message)
272
+ ex.set_backtrace(result.backtrace)
273
+ raise ex
274
+ end
275
+ end
276
+
277
+ def parse_addresses(addresses)
278
+ if addresses.empty?
279
+ # no address found, eliminate candidates
280
+ hostname, connection = @queries.first
281
+ reset_hostname(hostname)
282
+ @connections.delete(connection)
283
+ raise NativeResolveError.new(connection, connection.origin.host)
217
284
  else
218
285
  address = addresses.first
219
286
  name = address["name"]
@@ -221,20 +288,21 @@ module HTTPX
221
288
  connection = @queries.delete(name)
222
289
 
223
290
  unless connection
291
+ orig_name = name
224
292
  # absolute name
225
293
  name_labels = Resolv::DNS::Name.create(name).to_a
226
- name = @queries.keys.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
294
+ name = @queries.each_key.first { |hname| name_labels == Resolv::DNS::Name.create(hname).to_a }
227
295
 
228
296
  # probably a retried query for which there's an answer
229
- return unless name
297
+ unless name
298
+ @timeouts.delete(orig_name)
299
+ return
300
+ end
230
301
 
231
302
  address["name"] = name
232
303
  connection = @queries.delete(name)
233
304
  end
234
305
 
235
- # eliminate other candidates
236
- @queries.delete_if { |_, conn| connection == conn }
237
-
238
306
  if address.key?("alias") # CNAME
239
307
  # clean up intermediate queries
240
308
  @timeouts.delete(name) unless connection.origin.host == name
@@ -246,7 +314,7 @@ module HTTPX
246
314
  return
247
315
  end
248
316
  else
249
- @timeouts.delete(name)
317
+ reset_hostname(name, connection: connection)
250
318
  @timeouts.delete(connection.origin.host)
251
319
  @connections.delete(connection)
252
320
  Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
@@ -260,6 +328,7 @@ module HTTPX
260
328
 
261
329
  def resolve(connection = @connections.first, hostname = nil)
262
330
  raise Error, "no URI to resolve" unless connection
331
+
263
332
  return unless @write_buffer.empty?
264
333
 
265
334
  hostname ||= @queries.key(connection)
@@ -276,12 +345,19 @@ module HTTPX
276
345
  end
277
346
  log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
278
347
  begin
279
- @write_buffer << Resolver.encode_dns_query(hostname, type: @record_type)
348
+ @write_buffer << encode_dns_query(hostname)
280
349
  rescue Resolv::DNS::EncodeError => e
281
350
  emit_resolve_error(connection, hostname, e)
282
351
  end
283
352
  end
284
353
 
354
+ def encode_dns_query(hostname)
355
+ message_id = Resolver.generate_id
356
+ msg = Resolver.encode_dns_query(hostname, type: @record_type, message_id: message_id)
357
+ msg[0, 2] = [msg.size, message_id].pack("nn") if @socket_type == :tcp
358
+ msg
359
+ end
360
+
285
361
  def generate_candidates(name)
286
362
  return [name] if name.end_with?(".")
287
363
 
@@ -289,21 +365,25 @@ module HTTPX
289
365
  name_parts = name.scan(/[^.]+/)
290
366
  candidates = [name] if @ndots <= name_parts.size - 1
291
367
  candidates.concat(@search.map { |domain| [*name_parts, *domain].join(".") })
292
- candidates << name unless candidates.include?(name)
368
+ fname = "#{name}."
369
+ candidates << fname unless candidates.include?(fname)
293
370
 
294
371
  candidates
295
372
  end
296
373
 
297
374
  def build_socket
298
- return if @io
299
-
300
375
  ip, port = @nameserver[@ns_index]
301
376
  port ||= DNS_PORT
302
- uri = URI::Generic.build(scheme: "udp", port: port)
303
- uri.hostname = ip
304
- type = IO.registry(uri.scheme)
305
- log { "resolver: server: #{uri}..." }
306
- @io = type.new(uri, [IPAddr.new(ip)], @options)
377
+
378
+ case @socket_type
379
+ when :udp
380
+ log { "resolver: server: udp://#{ip}:#{port}..." }
381
+ UDP.new(ip, port, @options)
382
+ when :tcp
383
+ log { "resolver: server: tcp://#{ip}:#{port}..." }
384
+ origin = URI("tcp://#{ip}:#{port}")
385
+ TCP.new(origin, [ip], @options)
386
+ end
307
387
  end
308
388
 
309
389
  def transition(nextstate)
@@ -317,7 +397,7 @@ module HTTPX
317
397
  when :open
318
398
  return unless @state == :idle
319
399
 
320
- build_socket
400
+ @io ||= build_socket
321
401
 
322
402
  @io.connect
323
403
  return unless @io.connected?
@@ -344,5 +424,18 @@ module HTTPX
344
424
  end
345
425
  end
346
426
  end
427
+
428
+ def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
429
+ @timeouts.delete(hostname)
430
+ @timeouts.delete(hostname)
431
+
432
+ return unless connection && reset_candidates
433
+
434
+ # eliminate other candidates
435
+ candidates = @queries.select { |_, conn| connection == conn }.keys
436
+ @queries.delete_if { |h, _| candidates.include?(h) }
437
+ # reset timeouts
438
+ @timeouts.delete_if { |h, _| candidates.include?(h) }
439
+ end
347
440
  end
348
441
  end
@@ -38,6 +38,8 @@ module HTTPX
38
38
 
39
39
  def close; end
40
40
 
41
+ alias_method :terminate, :close
42
+
41
43
  def closed?
42
44
  true
43
45
  end
@@ -46,15 +48,15 @@ module HTTPX
46
48
  true
47
49
  end
48
50
 
49
- def emit_addresses(connection, family, addresses)
51
+ def emit_addresses(connection, family, addresses, early_resolve = false)
50
52
  addresses.map! do |address|
51
53
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
52
54
  end
53
55
 
54
- # double emission check
55
- return if connection.addresses && !addresses.intersect?(connection.addresses)
56
+ # double emission check, but allow early resolution to work
57
+ return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
56
58
 
57
- log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
59
+ log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
58
60
  if @pool && # if triggered by early resolve, pool may not be here yet
59
61
  !connection.io &&
60
62
  connection.options.ip_families.size > 1 &&
@@ -62,21 +64,34 @@ module HTTPX
62
64
  addresses.first.to_s != connection.origin.host.to_s
63
65
  log { "resolver: A response, applying resolution delay..." }
64
66
  @pool.after(0.05) do
65
- # double emission check
66
- unless connection.addresses && addresses.intersect?(connection.addresses)
67
-
68
- connection.addresses = addresses
69
- emit(:resolve, connection)
67
+ unless connection.state == :closed ||
68
+ # double emission check
69
+ (connection.addresses && addresses.intersect?(connection.addresses))
70
+ emit_resolved_connection(connection, addresses, early_resolve)
70
71
  end
71
72
  end
72
73
  else
73
- connection.addresses = addresses
74
- emit(:resolve, connection)
74
+ emit_resolved_connection(connection, addresses, early_resolve)
75
75
  end
76
76
  end
77
77
 
78
78
  private
79
79
 
80
+ def emit_resolved_connection(connection, addresses, early_resolve)
81
+ begin
82
+ connection.addresses = addresses
83
+
84
+ emit(:resolve, connection)
85
+ rescue StandardError => e
86
+ if early_resolve
87
+ connection.force_reset
88
+ throw(:resolve_error, e)
89
+ else
90
+ emit(:error, connection, e)
91
+ end
92
+ end
93
+ end
94
+
80
95
  def early_resolve(connection, hostname: connection.origin.host)
81
96
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
82
97
 
@@ -86,7 +101,7 @@ module HTTPX
86
101
 
87
102
  return if addresses.empty?
88
103
 
89
- emit_addresses(connection, @family, addresses)
104
+ emit_addresses(connection, @family, addresses, true)
90
105
  end
91
106
 
92
107
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -94,7 +109,7 @@ module HTTPX
94
109
  end
95
110
 
96
111
  def resolve_error(hostname, ex = nil)
97
- return ex if ex.is_a?(ResolveError)
112
+ return ex if ex.is_a?(ResolveError) || ex.is_a?(ResolveTimeoutError)
98
113
 
99
114
  message = ex ? ex.message : "Can't resolve #{hostname}"
100
115
  error = ResolveError.new(message)
@@ -92,6 +92,12 @@ module HTTPX
92
92
  resolve
93
93
  end
94
94
 
95
+ def handle_socket_timeout(interval)
96
+ error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
97
+ error.set_backtrace(caller)
98
+ on_error(error)
99
+ end
100
+
95
101
  private
96
102
 
97
103
  def transition(nextstate)
@@ -158,12 +164,14 @@ module HTTPX
158
164
  def async_resolve(connection, hostname, scheme)
159
165
  families = connection.options.ip_families
160
166
  log { "resolver: query for #{hostname}" }
161
- resolve_timeout = @timeouts[connection.origin.host].first
167
+ timeouts = @timeouts[connection.origin.host]
168
+ resolve_timeout = timeouts.first
162
169
 
163
170
  Thread.start do
164
171
  Thread.current.report_on_exception = false
165
172
  begin
166
173
  addrs = if resolve_timeout
174
+
167
175
  Timeout.timeout(resolve_timeout) do
168
176
  __addrinfo_resolve(hostname, scheme)
169
177
  end
@@ -182,16 +190,13 @@ module HTTPX
182
190
  @pipe_write.putc(DONE) unless @pipe_write.closed?
183
191
  end
184
192
  end
185
- rescue Timeout::Error => e
186
- ex = ResolveTimeoutError.new(resolve_timeout, e.message)
187
- ex.set_backtrace(ex.backtrace)
188
- @pipe_mutex.synchronize do
189
- families.each do |family|
190
- @ips.unshift([family, connection, ex])
191
- @pipe_write.putc(ERROR) unless @pipe_write.closed?
192
- end
193
- end
194
193
  rescue StandardError => e
194
+ if e.is_a?(Timeout::Error)
195
+ timeouts.shift
196
+ retry unless timeouts.empty?
197
+ e = ResolveTimeoutError.new(resolve_timeout, e.message)
198
+ e.set_backtrace(e.backtrace)
199
+ end
195
200
  @pipe_mutex.synchronize do
196
201
  families.each do |family|
197
202
  @ips.unshift([family, connection, e])
@@ -5,9 +5,7 @@ require "ipaddr"
5
5
 
6
6
  module HTTPX
7
7
  module Resolver
8
- extend Registry
9
-
10
- RESOLVE_TIMEOUT = 5
8
+ RESOLVE_TIMEOUT = [2, 3].freeze
11
9
 
12
10
  require "httpx/resolver/resolver"
13
11
  require "httpx/resolver/system"
@@ -15,19 +13,27 @@ module HTTPX
15
13
  require "httpx/resolver/https"
16
14
  require "httpx/resolver/multi"
17
15
 
18
- register :system, System
19
- register :native, Native
20
- register :https, HTTPS
21
-
22
- @lookup_mutex = Mutex.new
16
+ @lookup_mutex = Thread::Mutex.new
23
17
  @lookups = Hash.new { |h, k| h[k] = [] }
24
18
 
25
- @identifier_mutex = Mutex.new
19
+ @identifier_mutex = Thread::Mutex.new
26
20
  @identifier = 1
27
21
  @system_resolver = Resolv::Hosts.new
28
22
 
29
23
  module_function
30
24
 
25
+ def resolver_for(resolver_type)
26
+ case resolver_type
27
+ when :native then Native
28
+ when :system then System
29
+ when :https then HTTPS
30
+ else
31
+ return resolver_type if resolver_type.is_a?(Class) && resolver_type < Resolver
32
+
33
+ raise Error, "unsupported resolver type (#{resolver_type})"
34
+ end
35
+ end
36
+
31
37
  def nolookup_resolve(hostname)
32
38
  ip_resolve(hostname) || cached_lookup(hostname) || system_resolve(hostname)
33
39
  end
@@ -81,16 +87,18 @@ module HTTPX
81
87
  def lookup(hostname, ttl)
82
88
  return unless @lookups.key?(hostname)
83
89
 
84
- @lookups[hostname] = @lookups[hostname].select do |address|
90
+ entries = @lookups[hostname] = @lookups[hostname].select do |address|
85
91
  address["TTL"] > ttl
86
92
  end
87
- ips = @lookups[hostname].flat_map do |address|
93
+
94
+ ips = entries.flat_map do |address|
88
95
  if address.key?("alias")
89
96
  lookup(address["alias"], ttl)
90
97
  else
91
98
  IPAddr.new(address["data"])
92
99
  end
93
- end
100
+ end.compact
101
+
94
102
  ips unless ips.empty?
95
103
  end
96
104
 
@@ -98,17 +106,30 @@ module HTTPX
98
106
  @identifier_mutex.synchronize { @identifier = (@identifier + 1) & 0xFFFF }
99
107
  end
100
108
 
101
- def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A)
109
+ def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
102
110
  Resolv::DNS::Message.new.tap do |query|
103
- query.id = generate_id
111
+ query.id = message_id
104
112
  query.rd = 1
105
113
  query.add_question(hostname, type)
106
114
  end.encode
107
115
  end
108
116
 
109
117
  def decode_dns_answer(payload)
110
- message = Resolv::DNS::Message.decode(payload)
118
+ begin
119
+ message = Resolv::DNS::Message.decode(payload)
120
+ rescue Resolv::DNS::DecodeError => e
121
+ return :decode_error, e
122
+ end
123
+
124
+ # no domain was found
125
+ return :no_domain_found if message.rcode == Resolv::DNS::RCode::NXDomain
126
+
127
+ return :message_truncated if message.tc == 1
128
+
129
+ return :dns_error, message.rcode if message.rcode != Resolv::DNS::RCode::NoError
130
+
111
131
  addresses = []
132
+
112
133
  message.each_answer do |question, _, value|
113
134
  case value
114
135
  when Resolv::DNS::Resource::IN::CNAME
@@ -126,7 +147,8 @@ module HTTPX
126
147
  }
127
148
  end
128
149
  end
129
- addresses
150
+
151
+ [:ok, addresses]
130
152
  end
131
153
  end
132
154
  end