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
@@ -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