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
@@ -8,6 +8,8 @@ module HTTPX
8
8
  include Callbacks
9
9
  include Loggable
10
10
 
11
+ using ArrayExtensions::Intersect
12
+
11
13
  RECORD_TYPES = {
12
14
  Socket::AF_INET6 => Resolv::DNS::Resource::IN::AAAA,
13
15
  Socket::AF_INET => Resolv::DNS::Resource::IN::A,
@@ -31,11 +33,13 @@ module HTTPX
31
33
  def initialize(family, options)
32
34
  @family = family
33
35
  @record_type = RECORD_TYPES[family]
34
- @options = Options.new(options)
36
+ @options = options
35
37
  end
36
38
 
37
39
  def close; end
38
40
 
41
+ alias_method :terminate, :close
42
+
39
43
  def closed?
40
44
  true
41
45
  end
@@ -44,11 +48,15 @@ module HTTPX
44
48
  true
45
49
  end
46
50
 
47
- def emit_addresses(connection, family, addresses)
51
+ def emit_addresses(connection, family, addresses, early_resolve = false)
48
52
  addresses.map! do |address|
49
53
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
50
54
  end
51
- log { "resolver: answer #{connection.origin.host}: #{addresses.inspect}" }
55
+
56
+ # double emission check, but allow early resolution to work
57
+ return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
58
+
59
+ log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
52
60
  if @pool && # if triggered by early resolve, pool may not be here yet
53
61
  !connection.io &&
54
62
  connection.options.ip_families.size > 1 &&
@@ -56,17 +64,34 @@ module HTTPX
56
64
  addresses.first.to_s != connection.origin.host.to_s
57
65
  log { "resolver: A response, applying resolution delay..." }
58
66
  @pool.after(0.05) do
59
- connection.addresses = addresses
60
- 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)
71
+ end
61
72
  end
62
73
  else
63
- connection.addresses = addresses
64
- emit(:resolve, connection)
74
+ emit_resolved_connection(connection, addresses, early_resolve)
65
75
  end
66
76
  end
67
77
 
68
78
  private
69
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
+
70
95
  def early_resolve(connection, hostname: connection.origin.host)
71
96
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
72
97
 
@@ -76,7 +101,7 @@ module HTTPX
76
101
 
77
102
  return if addresses.empty?
78
103
 
79
- emit_addresses(connection, @family, addresses)
104
+ emit_addresses(connection, @family, addresses, true)
80
105
  end
81
106
 
82
107
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -84,7 +109,7 @@ module HTTPX
84
109
  end
85
110
 
86
111
  def resolve_error(hostname, ex = nil)
87
- return ex if ex.is_a?(ResolveError)
112
+ return ex if ex.is_a?(ResolveError) || ex.is_a?(ResolveTimeoutError)
88
113
 
89
114
  message = ex ? ex.message : "Can't resolve #{hostname}"
90
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)
@@ -121,7 +127,7 @@ module HTTPX
121
127
  @queries.delete(pair)
122
128
 
123
129
  family, connection = pair
124
- emit_addresses(connection, family, addrs)
130
+ catch(:coalesced) { emit_addresses(connection, family, addrs) }
125
131
  when ERROR
126
132
  *pair, error = @pipe_mutex.synchronize { @ips.pop }
127
133
  @queries.delete(pair)
@@ -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
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ # Implementation of the HTTP Response body as a buffer which implements the IO writer protocol
5
+ # (for buffering the response payload), the IO reader protocol (for consuming the response payload),
6
+ # and can be iterated over (via #each, which yields the payload in chunks).
7
+ class Response::Body
8
+ # the payload encoding (i.e. "utf-8", "ASCII-8BIT")
9
+ attr_reader :encoding
10
+
11
+ # Array of encodings contained in the response "content-encoding" header.
12
+ attr_reader :encodings
13
+
14
+ # initialized with the corresponding HTTPX::Response +response+ and HTTPX::Options +options+.
15
+ def initialize(response, options)
16
+ @response = response
17
+ @headers = response.headers
18
+ @options = options
19
+ @window_size = options.window_size
20
+ @encoding = response.content_type.charset || Encoding::BINARY
21
+ @encodings = []
22
+ @length = 0
23
+ @buffer = nil
24
+ @reader = nil
25
+ @state = :idle
26
+ initialize_inflaters
27
+ end
28
+
29
+ def initialize_dup(other)
30
+ super
31
+
32
+ @buffer = other.instance_variable_get(:@buffer).dup
33
+ end
34
+
35
+ def closed?
36
+ @state == :closed
37
+ end
38
+
39
+ # write the response payload +chunk+ into the buffer. Inflates the chunk when required
40
+ # and supported.
41
+ def write(chunk)
42
+ return if @state == :closed
43
+
44
+ return 0 if chunk.empty?
45
+
46
+ chunk = decode_chunk(chunk)
47
+
48
+ size = chunk.bytesize
49
+ @length += size
50
+ transition(:open)
51
+ @buffer.write(chunk)
52
+
53
+ @response.emit(:chunk_received, chunk)
54
+ size
55
+ end
56
+
57
+ # reads a chunk from the payload (implementation of the IO reader protocol).
58
+ def read(*args)
59
+ return unless @buffer
60
+
61
+ unless @reader
62
+ rewind
63
+ @reader = @buffer
64
+ end
65
+
66
+ @reader.read(*args)
67
+ end
68
+
69
+ # size of the decoded response payload. May differ from "content-length" header if
70
+ # response was encoded over-the-wire.
71
+ def bytesize
72
+ @length
73
+ end
74
+
75
+ # yields the payload in chunks.
76
+ def each
77
+ return enum_for(__method__) unless block_given?
78
+
79
+ begin
80
+ if @buffer
81
+ rewind
82
+ while (chunk = @buffer.read(@window_size))
83
+ yield(chunk.force_encoding(@encoding))
84
+ end
85
+ end
86
+ ensure
87
+ close
88
+ end
89
+ end
90
+
91
+ # returns the declared filename in the "contennt-disposition" header, when present.
92
+ def filename
93
+ return unless @headers.key?("content-disposition")
94
+
95
+ Utils.get_filename(@headers["content-disposition"])
96
+ end
97
+
98
+ # returns the full response payload as a string.
99
+ def to_s
100
+ return "".b unless @buffer
101
+
102
+ @buffer.to_s
103
+ end
104
+
105
+ alias_method :to_str, :to_s
106
+
107
+ # whether the payload is empty.
108
+ def empty?
109
+ @length.zero?
110
+ end
111
+
112
+ # copies the payload to +dest+.
113
+ #
114
+ # body.copy_to("path/to/file")
115
+ # body.copy_to(Pathname.new("path/to/file"))
116
+ # body.copy_to(File.new("path/to/file"))
117
+ def copy_to(dest)
118
+ return unless @buffer
119
+
120
+ rewind
121
+
122
+ if dest.respond_to?(:path) && @buffer.respond_to?(:path)
123
+ FileUtils.mv(@buffer.path, dest.path)
124
+ else
125
+ ::IO.copy_stream(@buffer, dest)
126
+ end
127
+ end
128
+
129
+ # closes/cleans the buffer, resets everything
130
+ def close
131
+ if @buffer
132
+ @buffer.close
133
+ @buffer = nil
134
+ end
135
+ @length = 0
136
+ transition(:closed)
137
+ end
138
+
139
+ def ==(other)
140
+ object_id == other.object_id || begin
141
+ if other.respond_to?(:read)
142
+ _with_same_buffer_pos { FileUtils.compare_stream(@buffer, other) }
143
+ else
144
+ to_s == other.to_s
145
+ end
146
+ end
147
+ end
148
+
149
+ # :nocov:
150
+ def inspect
151
+ "#<HTTPX::Response::Body:#{object_id} " \
152
+ "@state=#{@state} " \
153
+ "@length=#{@length}>"
154
+ end
155
+ # :nocov:
156
+
157
+ # rewinds the response payload buffer.
158
+ def rewind
159
+ return unless @buffer
160
+
161
+ # in case there's some reading going on
162
+ @reader = nil
163
+
164
+ @buffer.rewind
165
+ end
166
+
167
+ private
168
+
169
+ # prepares inflaters for the advertised encodings in "content-encoding" header.
170
+ def initialize_inflaters
171
+ @inflaters = nil
172
+
173
+ return unless @headers.key?("content-encoding")
174
+
175
+ return unless @options.decompress_response_body
176
+
177
+ @inflaters = @headers.get("content-encoding").filter_map do |encoding|
178
+ next if encoding == "identity"
179
+
180
+ inflater = self.class.initialize_inflater_by_encoding(encoding, @response)
181
+
182
+ # do not uncompress if there is no decoder available. In fact, we can't reliably
183
+ # continue decompressing beyond that, so ignore.
184
+ break unless inflater
185
+
186
+ @encodings << encoding
187
+ inflater
188
+ end
189
+ end
190
+
191
+ # passes the +chunk+ through all inflaters to decode it.
192
+ def decode_chunk(chunk)
193
+ @inflaters.reverse_each do |inflater|
194
+ chunk = inflater.call(chunk)
195
+ end if @inflaters
196
+
197
+ chunk
198
+ end
199
+
200
+ # tries transitioning the body STM to the +nextstate+.
201
+ def transition(nextstate)
202
+ case nextstate
203
+ when :open
204
+ return unless @state == :idle
205
+
206
+ @buffer = Response::Buffer.new(
207
+ threshold_size: @options.body_threshold_size,
208
+ bytesize: @length,
209
+ encoding: @encoding
210
+ )
211
+ when :closed
212
+ return if @state == :closed
213
+ end
214
+
215
+ @state = nextstate
216
+ end
217
+
218
+ def _with_same_buffer_pos # :nodoc:
219
+ return yield unless @buffer && @buffer.respond_to?(:pos)
220
+
221
+ # @type ivar @buffer: StringIO | Tempfile
222
+ current_pos = @buffer.pos
223
+ @buffer.rewind
224
+ begin
225
+ yield
226
+ ensure
227
+ @buffer.pos = current_pos
228
+ end
229
+ end
230
+
231
+ class << self
232
+ def initialize_inflater_by_encoding(encoding, response, **kwargs) # :nodoc:
233
+ case encoding
234
+ when "gzip"
235
+ Transcoder::GZIP.decode(response, **kwargs)
236
+ when "deflate"
237
+ Transcoder::Deflate.decode(response, **kwargs)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "stringio"
5
+ require "tempfile"
6
+
7
+ module HTTPX
8
+ # wraps and delegates to an internal buffer, which can be a StringIO or a Tempfile.
9
+ class Response::Buffer < SimpleDelegator
10
+ # initializes buffer with the +threshold_size+ over which the payload gets buffer to a tempfile,
11
+ # the initial +bytesize+, and the +encoding+.
12
+ def initialize(threshold_size:, bytesize: 0, encoding: Encoding::BINARY)
13
+ @threshold_size = threshold_size
14
+ @bytesize = bytesize
15
+ @encoding = encoding
16
+ @buffer = StringIO.new("".b)
17
+ super(@buffer)
18
+ end
19
+
20
+ def initialize_dup(other)
21
+ super
22
+
23
+ @buffer = other.instance_variable_get(:@buffer).dup
24
+ end
25
+
26
+ # size in bytes of the buffered content.
27
+ def size
28
+ @bytesize
29
+ end
30
+
31
+ # writes the +chunk+ into the buffer.
32
+ def write(chunk)
33
+ @bytesize += chunk.bytesize
34
+ try_upgrade_buffer
35
+ @buffer.write(chunk)
36
+ end
37
+
38
+ # returns the buffered content as a string.
39
+ def to_s
40
+ case @buffer
41
+ when StringIO
42
+ begin
43
+ @buffer.string.force_encoding(@encoding)
44
+ rescue ArgumentError
45
+ @buffer.string
46
+ end
47
+ when Tempfile
48
+ rewind
49
+ content = _with_same_buffer_pos { @buffer.read }
50
+ begin
51
+ content.force_encoding(@encoding)
52
+ rescue ArgumentError # ex: unknown encoding name - utf
53
+ content
54
+ end
55
+ end
56
+ end
57
+
58
+ # closes the buffer.
59
+ def close
60
+ @buffer.close
61
+ @buffer.unlink if @buffer.respond_to?(:unlink)
62
+ end
63
+
64
+ private
65
+
66
+ # initializes the buffer into a StringIO, or turns it into a Tempfile when the threshold
67
+ # has been reached.
68
+ def try_upgrade_buffer
69
+ return unless @bytesize > @threshold_size
70
+
71
+ return if @buffer.is_a?(Tempfile)
72
+
73
+ aux = @buffer
74
+
75
+ @buffer = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
76
+
77
+ if aux
78
+ aux.rewind
79
+ ::IO.copy_stream(aux, @buffer)
80
+ aux.close
81
+ end
82
+
83
+ __setobj__(@buffer)
84
+ end
85
+
86
+ def _with_same_buffer_pos # :nodoc:
87
+ current_pos = @buffer.pos
88
+ @buffer.rewind
89
+ begin
90
+ yield
91
+ ensure
92
+ @buffer.pos = current_pos
93
+ end
94
+ end
95
+ end
96
+ end