httpx 0.21.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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