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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- class HTTPProxyError < Error; end
4
+ class HTTPProxyError < ConnectionError; end
5
5
 
6
6
  module Plugins
7
7
  #
@@ -12,12 +12,24 @@ module HTTPX
12
12
  # * Socks4/4a proxies
13
13
  # * Socks5 proxies
14
14
  #
15
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Proxy
15
+ # https://gitlab.com/os85/httpx/wikis/Proxy
16
16
  #
17
17
  module Proxy
18
18
  Error = HTTPProxyError
19
19
  PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
20
20
 
21
+ class << self
22
+ def configure(klass)
23
+ klass.plugin(:"proxy/http")
24
+ klass.plugin(:"proxy/socks4")
25
+ klass.plugin(:"proxy/socks5")
26
+ end
27
+
28
+ def extra_options(options)
29
+ options.merge(supported_proxy_protocols: [])
30
+ end
31
+ end
32
+
21
33
  class Parameters
22
34
  attr_reader :uri, :username, :password, :scheme
23
35
 
@@ -41,7 +53,7 @@ module HTTPX
41
53
 
42
54
  auth_scheme = scheme.to_s.capitalize
43
55
 
44
- require_relative "authentication/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
56
+ require_relative "auth/#{scheme}" unless defined?(Authentication) && Authentication.const_defined?(auth_scheme, false)
45
57
 
46
58
  @authenticator = Authentication.const_get(auth_scheme).new(@username, @password, **extra)
47
59
  end
@@ -77,48 +89,35 @@ module HTTPX
77
89
  end
78
90
  end
79
91
 
80
- class << self
81
- def configure(klass)
82
- klass.plugin(:"proxy/http")
83
- klass.plugin(:"proxy/socks4")
84
- klass.plugin(:"proxy/socks5")
85
- end
86
- end
87
-
92
+ # adds support for the following options:
93
+ #
94
+ # :proxy :: proxy options defining *:uri*, *:username*, *:password* or
95
+ # *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
88
96
  module OptionsMethods
89
97
  def option_proxy(value)
90
98
  value.is_a?(Parameters) ? value : Hash[value]
91
99
  end
100
+
101
+ def option_supported_proxy_protocols(value)
102
+ raise TypeError, ":supported_proxy_protocols must be an Array" unless value.is_a?(Array)
103
+
104
+ value.map(&:to_s)
105
+ end
92
106
  end
93
107
 
94
108
  module InstanceMethods
95
109
  private
96
110
 
97
- def proxy_uris(uri, options)
98
- @_proxy_uris ||= begin
99
- uris = options.proxy ? Array(options.proxy[:uri]) : []
100
- if uris.empty?
101
- uri = URI(uri).find_proxy
102
- uris << uri if uri
103
- end
104
- uris
105
- end
106
- return if @_proxy_uris.empty?
107
-
108
- proxy_opts = { uri: @_proxy_uris.first }
109
- proxy_opts = options.proxy.merge(proxy_opts) if options.proxy
110
- proxy_opts
111
- end
112
-
113
111
  def find_connection(request, connections, options)
114
112
  return super unless options.respond_to?(:proxy)
115
113
 
116
- uri = URI(request.uri)
117
- next_proxy = proxy_uris(uri, options)
118
- raise Error, "Failed to connect to proxy" unless next_proxy
114
+ uri = request.uri
115
+
116
+ proxy_options = proxy_options(uri, options)
117
+
118
+ return super(request, connections, proxy_options) unless proxy_options.proxy
119
119
 
120
- proxy_options = options.merge(proxy: Parameters.new(**next_proxy))
121
- connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
120
+ connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
122
121
  unless connections.nil? || connections.include?(connection)
123
122
  connections << connection
124
123
  set_connection_callbacks(connection, connections, options)
@@ -126,40 +125,61 @@ module HTTPX
126
125
  connection
127
126
  end
128
127
 
129
- def build_connection(uri, options)
130
- proxy = options.proxy
131
- return super unless proxy
128
+ def proxy_options(request_uri, options)
129
+ proxy_opts = if (next_proxy = request_uri.find_proxy)
130
+ { uri: next_proxy }
131
+ else
132
+ proxy = options.proxy
133
+
134
+ return options unless proxy
135
+
136
+ return options.merge(proxy: nil) unless proxy.key?(:uri)
137
+
138
+ @_proxy_uris ||= Array(proxy[:uri])
139
+
140
+ next_proxy = @_proxy_uris.first
141
+ raise Error, "Failed to connect to proxy" unless next_proxy
142
+
143
+ next_proxy = URI(next_proxy)
144
+
145
+ raise Error,
146
+ "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
132
147
 
133
- connection = options.connection_class.new("tcp", uri, options)
134
- catch(:coalesced) do
135
- pool.init_connection(connection, options)
136
- connection
148
+ if proxy.key?(:no_proxy)
149
+
150
+ no_proxy = proxy[:no_proxy]
151
+ no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
152
+
153
+ return options.merge(proxy: nil) unless URI::Generic.use_proxy?(request_uri.host, next_proxy.host,
154
+ next_proxy.port, no_proxy)
155
+ end
156
+
157
+ proxy.merge(uri: next_proxy)
137
158
  end
159
+
160
+ proxy = Parameters.new(**proxy_opts)
161
+
162
+ options.merge(proxy: proxy)
138
163
  end
139
164
 
140
165
  def fetch_response(request, connections, options)
141
166
  response = super
142
167
 
143
- if response.is_a?(ErrorResponse) &&
144
- __proxy_error?(response) && !@_proxy_uris.empty?
168
+ if response.is_a?(ErrorResponse) && proxy_error?(request, response)
145
169
  @_proxy_uris.shift
170
+
171
+ # return last error response if no more proxies to try
172
+ return response if @_proxy_uris.empty?
173
+
146
174
  log { "failed connecting to proxy, trying next..." }
147
175
  request.transition(:idle)
148
- connection = find_connection(request, connections, options)
149
- connections << connection unless connections.include?(connection)
150
- connection.send(request)
176
+ send_request(request, connections, options)
151
177
  return
152
178
  end
153
179
  response
154
180
  end
155
181
 
156
- def build_altsvc_connection(_, _, _, _, _, options)
157
- return if options.proxy
158
-
159
- super
160
- end
161
-
162
- def __proxy_error?(response)
182
+ def proxy_error?(_request, response)
163
183
  error = response.error
164
184
  case error
165
185
  when NativeResolveError
@@ -216,13 +236,6 @@ module HTTPX
216
236
  end
217
237
  end
218
238
 
219
- def send(request)
220
- return super unless @options.proxy
221
- return super unless connecting?
222
-
223
- @pending << request
224
- end
225
-
226
239
  def connecting?
227
240
  return super unless @options.proxy
228
241
 
@@ -244,13 +257,19 @@ module HTTPX
244
257
  return super unless @options.proxy
245
258
 
246
259
  @state = :open
247
- transition(:closing)
248
- transition(:closed)
260
+
261
+ super
249
262
  emit(:close)
250
263
  end
251
264
 
252
265
  private
253
266
 
267
+ def initialize_type(uri, options)
268
+ return super unless options.proxy
269
+
270
+ "tcp"
271
+ end
272
+
254
273
  def connect
255
274
  return super unless @options.proxy
256
275
 
@@ -278,7 +297,7 @@ module HTTPX
278
297
  register_plugin :proxy, Proxy
279
298
  end
280
299
 
281
- class ProxySSL < IO.registry["ssl"]
300
+ class ProxySSL < SSL
282
301
  def initialize(tcp, request_uri, options)
283
302
  @io = tcp.to_io
284
303
  super(request_uri, tcp.addresses, options)
@@ -8,7 +8,7 @@ module HTTPX
8
8
  # In order to benefit from this, requests are sent one at a time, so that
9
9
  # no push responses are received after corresponding request has been sent.
10
10
  #
11
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Server-Push
11
+ # https://gitlab.com/os85/httpx/wikis/Server-Push
12
12
  #
13
13
  module PushPromise
14
14
  def self.extra_options(options)
@@ -9,7 +9,7 @@ module HTTPX
9
9
  # * when the server is unavailable (503);
10
10
  # * when a 3xx request comes with a "retry-after" value
11
11
  #
12
- # https://gitlab.com/honeyryderchuck/httpx/wikis/RateLimiter
12
+ # https://gitlab.com/os85/httpx/wikis/Rate-Limiter
13
13
  #
14
14
  module RateLimiter
15
15
  class << self
@@ -23,6 +23,8 @@ module HTTPX
23
23
  end
24
24
 
25
25
  def retry_on_rate_limited_response(response)
26
+ return false unless response.is_a?(Response)
27
+
26
28
  status = response.status
27
29
 
28
30
  RATE_LIMIT_CODES.include?(status)
@@ -37,6 +39,8 @@ module HTTPX
37
39
  # the redirected request.
38
40
  #
39
41
  def retry_after_rate_limit(_, response)
42
+ return unless response.is_a?(Response)
43
+
40
44
  retry_after = response.headers["retry-after"]
41
45
 
42
46
  return unless retry_after
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "store"
5
+
6
+ module HTTPX::Plugins
7
+ module ResponseCache
8
+ class FileStore < Store
9
+ def initialize(dir = Dir.tmpdir)
10
+ @dir = Pathname.new(dir)
11
+ end
12
+
13
+ def clear
14
+ # delete all files
15
+ end
16
+
17
+ def cached?(request)
18
+ file_path = @dir.join(request.response_cache_key)
19
+
20
+ exist?(file_path)
21
+ end
22
+
23
+ private
24
+
25
+ def _get(request)
26
+ return unless cached?(request)
27
+
28
+ File.open(@dir.join(request.response_cache_key))
29
+ end
30
+
31
+ def _set(request, response)
32
+ file_path = @dir.join(request.response_cache_key)
33
+
34
+ response.copy_to(file_path)
35
+
36
+ response.body.rewind
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,47 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module HTTPX::Plugins
6
4
  module ResponseCache
7
5
  class Store
8
- extend Forwardable
9
-
10
- def_delegator :@store, :clear
11
-
12
6
  def initialize
13
7
  @store = {}
8
+ @store_mutex = Thread::Mutex.new
9
+ end
10
+
11
+ def clear
12
+ @store_mutex.synchronize { @store.clear }
14
13
  end
15
14
 
16
- def lookup(uri)
17
- @store[uri]
15
+ def lookup(request)
16
+ responses = _get(request)
17
+
18
+ return unless responses
19
+
20
+ responses.find(&method(:match_by_vary?).curry(2)[request])
18
21
  end
19
22
 
20
- def cached?(uri)
21
- @store.key?(uri)
23
+ def cached?(request)
24
+ lookup(request)
22
25
  end
23
26
 
24
- def cache(uri, response)
25
- @store[uri] = response
27
+ def cache(request, response)
28
+ return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
29
+
30
+ _set(request, response)
26
31
  end
27
32
 
28
33
  def prepare(request)
29
- cached_response = @store[request.uri]
34
+ cached_response = lookup(request)
30
35
 
31
36
  return unless cached_response
32
37
 
33
- original_request = cached_response.instance_variable_get(:@request)
34
-
35
- if (vary = cached_response.headers["vary"])
36
- if vary == "*"
37
- return unless request.headers.same_headers?(original_request.headers)
38
- else
39
- return unless vary.split(/ *, */).all? do |cache_field|
40
- cache_field.downcase!
41
- !original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
42
- end
43
- end
44
- end
38
+ return unless match_by_vary?(request, cached_response)
45
39
 
46
40
  if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
47
41
  request.headers.add("if-modified-since", last_modified)
@@ -51,6 +45,49 @@ module HTTPX::Plugins
51
45
  request.headers.add("if-none-match", etag)
52
46
  end
53
47
  end
48
+
49
+ private
50
+
51
+ def match_by_vary?(request, response)
52
+ vary = response.vary
53
+
54
+ return true unless vary
55
+
56
+ original_request = response.instance_variable_get(:@request)
57
+
58
+ return request.headers.same_headers?(original_request.headers) if vary == %w[*]
59
+
60
+ vary.all? do |cache_field|
61
+ cache_field.downcase!
62
+ !original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
63
+ end
64
+ end
65
+
66
+ def _get(request)
67
+ @store_mutex.synchronize do
68
+ responses = @store[request.response_cache_key]
69
+
70
+ return unless responses
71
+
72
+ responses.select! do |res|
73
+ !res.body.closed? && res.fresh?
74
+ end
75
+
76
+ responses
77
+ end
78
+ end
79
+
80
+ def _set(request, response)
81
+ @store_mutex.synchronize do
82
+ responses = (@store[request.response_cache_key] ||= [])
83
+
84
+ responses.reject! do |res|
85
+ res.body.closed? || !res.fresh? || match_by_vary?(request, res)
86
+ end
87
+
88
+ responses << response
89
+ end
90
+ end
54
91
  end
55
92
  end
56
93
  end
@@ -5,11 +5,13 @@ module HTTPX
5
5
  #
6
6
  # This plugin adds support for retrying requests when certain errors happen.
7
7
  #
8
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Response-Cache
8
+ # https://gitlab.com/os85/httpx/wikis/Response-Cache
9
9
  #
10
10
  module ResponseCache
11
- CACHEABLE_VERBS = %i[get head].freeze
11
+ CACHEABLE_VERBS = %w[GET HEAD].freeze
12
+ CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
12
13
  private_constant :CACHEABLE_VERBS
14
+ private_constant :CACHEABLE_STATUS_CODES
13
15
 
14
16
  class << self
15
17
  def load_dependencies(*)
@@ -17,14 +19,28 @@ module HTTPX
17
19
  end
18
20
 
19
21
  def cacheable_request?(request)
20
- CACHEABLE_VERBS.include?(request.verb)
22
+ CACHEABLE_VERBS.include?(request.verb) &&
23
+ (
24
+ !request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
25
+ )
21
26
  end
22
27
 
23
28
  def cacheable_response?(response)
24
29
  response.is_a?(Response) &&
25
- # partial responses shall not be cached, only full ones.
30
+ (
31
+ response.cache_control.nil? ||
32
+ # TODO: !response.cache_control.include?("private") && is shared cache
33
+ !response.cache_control.include?("no-store")
34
+ ) &&
35
+ CACHEABLE_STATUS_CODES.include?(response.status) &&
36
+ # RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
37
+ # 410 MAY be stored by a cache and used in reply to a subsequent
38
+ # request, subject to the expiration mechanism, unless a cache-control
39
+ # directive prohibits caching. However, a cache that does not support
40
+ # the Range and Content-Range headers MUST NOT cache 206 (Partial
41
+ # Content) responses.
26
42
  response.status != 206 && (
27
- response.headers.key?("etag") || response.headers.key?("last-modified-at")
43
+ response.headers.key?("etag") || response.headers.key?("last-modified") || response.fresh?
28
44
  )
29
45
  end
30
46
 
@@ -52,7 +68,7 @@ module HTTPX
52
68
 
53
69
  def build_request(*)
54
70
  request = super
55
- return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request.uri)
71
+ return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request)
56
72
 
57
73
  @options.response_cache_store.prepare(request)
58
74
 
@@ -62,24 +78,101 @@ module HTTPX
62
78
  def fetch_response(request, *)
63
79
  response = super
64
80
 
65
- if response && ResponseCache.cached_response?(response)
81
+ return unless response
82
+
83
+ if ResponseCache.cached_response?(response)
66
84
  log { "returning cached response for #{request.uri}" }
67
- cached_response = @options.response_cache_store.lookup(request.uri)
85
+ cached_response = @options.response_cache_store.lookup(request)
68
86
 
69
87
  response.copy_from_cached(cached_response)
70
- end
71
88
 
72
- @options.response_cache_store.cache(request.uri, response) if response && ResponseCache.cacheable_response?(response)
89
+ else
90
+ @options.response_cache_store.cache(request, response)
91
+ end
73
92
 
74
93
  response
75
94
  end
76
95
  end
77
96
 
97
+ module RequestMethods
98
+ def response_cache_key
99
+ @response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}-#{@uri}")
100
+ end
101
+ end
102
+
78
103
  module ResponseMethods
79
104
  def copy_from_cached(other)
80
- @body = other.body
105
+ # 304 responses do not have content-type, which are needed for decoding.
106
+ @headers = @headers.class.new(other.headers.merge(@headers))
107
+
108
+ @body = other.body.dup
109
+
110
+ @body.rewind
111
+ end
112
+
113
+ # A response is fresh if its age has not yet exceeded its freshness lifetime.
114
+ def fresh?
115
+ if cache_control
116
+ return false if cache_control.include?("no-cache")
117
+
118
+ # check age: max-age
119
+ max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
120
+
121
+ max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
122
+
123
+ max_age = max_age[/age=(\d+)/, 1] if max_age
124
+
125
+ max_age = max_age.to_i if max_age
126
+
127
+ return max_age > age if max_age
128
+ end
129
+
130
+ # check age: expires
131
+ if @headers.key?("expires")
132
+ begin
133
+ expires = Time.httpdate(@headers["expires"])
134
+ rescue ArgumentError
135
+ return true
136
+ end
137
+
138
+ return (expires - Time.now).to_i.positive?
139
+ end
140
+
141
+ true
142
+ end
143
+
144
+ def cache_control
145
+ return @cache_control if defined?(@cache_control)
146
+
147
+ @cache_control = begin
148
+ return unless @headers.key?("cache-control")
149
+
150
+ @headers["cache-control"].split(/ *, */)
151
+ end
152
+ end
153
+
154
+ def vary
155
+ return @vary if defined?(@vary)
156
+
157
+ @vary = begin
158
+ return unless @headers.key?("vary")
159
+
160
+ @headers["vary"].split(/ *, */)
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def age
167
+ return @headers["age"].to_i if @headers.key?("age")
168
+
169
+ (Time.now - date).to_i
170
+ end
81
171
 
82
- @body.__send__(:rewind)
172
+ def date
173
+ @date ||= Time.httpdate(@headers["date"])
174
+ rescue NoMethodError, ArgumentError
175
+ Time.now
83
176
  end
84
177
  end
85
178
  end