httpx 0.20.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (250) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +5 -5
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -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