httpx-patched 1.6.2.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 (336) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +191 -0
  3. data/README.md +162 -0
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_10_1.md +37 -0
  11. data/doc/release_notes/0_10_2.md +5 -0
  12. data/doc/release_notes/0_11_0.md +74 -0
  13. data/doc/release_notes/0_11_1.md +5 -0
  14. data/doc/release_notes/0_11_2.md +5 -0
  15. data/doc/release_notes/0_11_3.md +5 -0
  16. data/doc/release_notes/0_12_0.md +55 -0
  17. data/doc/release_notes/0_13_0.md +58 -0
  18. data/doc/release_notes/0_13_1.md +5 -0
  19. data/doc/release_notes/0_13_2.md +9 -0
  20. data/doc/release_notes/0_14_0.md +79 -0
  21. data/doc/release_notes/0_14_1.md +7 -0
  22. data/doc/release_notes/0_14_2.md +6 -0
  23. data/doc/release_notes/0_14_3.md +5 -0
  24. data/doc/release_notes/0_14_4.md +5 -0
  25. data/doc/release_notes/0_14_5.md +11 -0
  26. data/doc/release_notes/0_15_0.md +53 -0
  27. data/doc/release_notes/0_15_1.md +8 -0
  28. data/doc/release_notes/0_15_2.md +9 -0
  29. data/doc/release_notes/0_15_3.md +5 -0
  30. data/doc/release_notes/0_15_4.md +5 -0
  31. data/doc/release_notes/0_16_0.md +93 -0
  32. data/doc/release_notes/0_16_1.md +5 -0
  33. data/doc/release_notes/0_17_0.md +49 -0
  34. data/doc/release_notes/0_18_0.md +69 -0
  35. data/doc/release_notes/0_18_1.md +12 -0
  36. data/doc/release_notes/0_18_2.md +10 -0
  37. data/doc/release_notes/0_18_3.md +7 -0
  38. data/doc/release_notes/0_18_4.md +14 -0
  39. data/doc/release_notes/0_18_5.md +10 -0
  40. data/doc/release_notes/0_18_6.md +5 -0
  41. data/doc/release_notes/0_18_7.md +5 -0
  42. data/doc/release_notes/0_19_0.md +39 -0
  43. data/doc/release_notes/0_19_1.md +5 -0
  44. data/doc/release_notes/0_19_2.md +7 -0
  45. data/doc/release_notes/0_19_3.md +6 -0
  46. data/doc/release_notes/0_19_4.md +14 -0
  47. data/doc/release_notes/0_19_5.md +13 -0
  48. data/doc/release_notes/0_19_6.md +5 -0
  49. data/doc/release_notes/0_19_7.md +5 -0
  50. data/doc/release_notes/0_19_8.md +5 -0
  51. data/doc/release_notes/0_1_0.md +9 -0
  52. data/doc/release_notes/0_20_0.md +36 -0
  53. data/doc/release_notes/0_20_1.md +5 -0
  54. data/doc/release_notes/0_20_2.md +7 -0
  55. data/doc/release_notes/0_20_3.md +6 -0
  56. data/doc/release_notes/0_20_4.md +17 -0
  57. data/doc/release_notes/0_20_5.md +3 -0
  58. data/doc/release_notes/0_21_0.md +96 -0
  59. data/doc/release_notes/0_21_1.md +12 -0
  60. data/doc/release_notes/0_22_0.md +13 -0
  61. data/doc/release_notes/0_22_1.md +11 -0
  62. data/doc/release_notes/0_22_2.md +5 -0
  63. data/doc/release_notes/0_22_3.md +55 -0
  64. data/doc/release_notes/0_22_4.md +6 -0
  65. data/doc/release_notes/0_22_5.md +6 -0
  66. data/doc/release_notes/0_23_0.md +42 -0
  67. data/doc/release_notes/0_23_1.md +5 -0
  68. data/doc/release_notes/0_23_2.md +5 -0
  69. data/doc/release_notes/0_23_3.md +6 -0
  70. data/doc/release_notes/0_23_4.md +5 -0
  71. data/doc/release_notes/0_24_0.md +48 -0
  72. data/doc/release_notes/0_24_1.md +12 -0
  73. data/doc/release_notes/0_24_2.md +12 -0
  74. data/doc/release_notes/0_24_3.md +12 -0
  75. data/doc/release_notes/0_24_4.md +18 -0
  76. data/doc/release_notes/0_24_5.md +6 -0
  77. data/doc/release_notes/0_24_6.md +5 -0
  78. data/doc/release_notes/0_24_7.md +10 -0
  79. data/doc/release_notes/0_2_0.md +5 -0
  80. data/doc/release_notes/0_2_1.md +16 -0
  81. data/doc/release_notes/0_3_0.md +12 -0
  82. data/doc/release_notes/0_3_1.md +6 -0
  83. data/doc/release_notes/0_4_0.md +51 -0
  84. data/doc/release_notes/0_4_1.md +3 -0
  85. data/doc/release_notes/0_5_0.md +15 -0
  86. data/doc/release_notes/0_5_1.md +14 -0
  87. data/doc/release_notes/0_6_0.md +5 -0
  88. data/doc/release_notes/0_6_1.md +6 -0
  89. data/doc/release_notes/0_6_2.md +6 -0
  90. data/doc/release_notes/0_6_3.md +13 -0
  91. data/doc/release_notes/0_6_4.md +21 -0
  92. data/doc/release_notes/0_6_5.md +22 -0
  93. data/doc/release_notes/0_6_6.md +19 -0
  94. data/doc/release_notes/0_6_7.md +5 -0
  95. data/doc/release_notes/0_7_0.md +46 -0
  96. data/doc/release_notes/0_8_0.md +27 -0
  97. data/doc/release_notes/0_8_1.md +8 -0
  98. data/doc/release_notes/0_8_2.md +7 -0
  99. data/doc/release_notes/0_9_0.md +38 -0
  100. data/doc/release_notes/1_0_0.md +60 -0
  101. data/doc/release_notes/1_0_1.md +5 -0
  102. data/doc/release_notes/1_0_2.md +7 -0
  103. data/doc/release_notes/1_1_0.md +32 -0
  104. data/doc/release_notes/1_1_1.md +17 -0
  105. data/doc/release_notes/1_1_2.md +12 -0
  106. data/doc/release_notes/1_1_3.md +18 -0
  107. data/doc/release_notes/1_1_4.md +6 -0
  108. data/doc/release_notes/1_1_5.md +12 -0
  109. data/doc/release_notes/1_2_0.md +49 -0
  110. data/doc/release_notes/1_2_1.md +6 -0
  111. data/doc/release_notes/1_2_2.md +10 -0
  112. data/doc/release_notes/1_2_3.md +16 -0
  113. data/doc/release_notes/1_2_4.md +8 -0
  114. data/doc/release_notes/1_2_5.md +7 -0
  115. data/doc/release_notes/1_2_6.md +13 -0
  116. data/doc/release_notes/1_3_0.md +18 -0
  117. data/doc/release_notes/1_3_1.md +17 -0
  118. data/doc/release_notes/1_3_2.md +6 -0
  119. data/doc/release_notes/1_3_3.md +5 -0
  120. data/doc/release_notes/1_3_4.md +6 -0
  121. data/doc/release_notes/1_4_0.md +43 -0
  122. data/doc/release_notes/1_4_1.md +19 -0
  123. data/doc/release_notes/1_4_2.md +20 -0
  124. data/doc/release_notes/1_4_3.md +11 -0
  125. data/doc/release_notes/1_4_4.md +14 -0
  126. data/doc/release_notes/1_5_0.md +126 -0
  127. data/doc/release_notes/1_5_1.md +6 -0
  128. data/doc/release_notes/1_6_0.md +50 -0
  129. data/doc/release_notes/1_6_1.md +17 -0
  130. data/doc/release_notes/1_6_2.md +11 -0
  131. data/lib/httpx/adapters/datadog.rb +359 -0
  132. data/lib/httpx/adapters/faraday.rb +303 -0
  133. data/lib/httpx/adapters/sentry.rb +121 -0
  134. data/lib/httpx/adapters/webmock.rb +175 -0
  135. data/lib/httpx/altsvc.rb +163 -0
  136. data/lib/httpx/base64.rb +27 -0
  137. data/lib/httpx/buffer.rb +61 -0
  138. data/lib/httpx/callbacks.rb +35 -0
  139. data/lib/httpx/chainable.rb +106 -0
  140. data/lib/httpx/connection/http1.rb +399 -0
  141. data/lib/httpx/connection/http2.rb +468 -0
  142. data/lib/httpx/connection.rb +954 -0
  143. data/lib/httpx/domain_name.rb +145 -0
  144. data/lib/httpx/errors.rb +111 -0
  145. data/lib/httpx/extensions.rb +59 -0
  146. data/lib/httpx/headers.rb +176 -0
  147. data/lib/httpx/io/ssl.rb +163 -0
  148. data/lib/httpx/io/tcp.rb +239 -0
  149. data/lib/httpx/io/udp.rb +62 -0
  150. data/lib/httpx/io/unix.rb +71 -0
  151. data/lib/httpx/io.rb +11 -0
  152. data/lib/httpx/loggable.rb +56 -0
  153. data/lib/httpx/options.rb +463 -0
  154. data/lib/httpx/parser/http1.rb +186 -0
  155. data/lib/httpx/plugins/auth/basic.rb +20 -0
  156. data/lib/httpx/plugins/auth/digest.rb +102 -0
  157. data/lib/httpx/plugins/auth/ntlm.rb +35 -0
  158. data/lib/httpx/plugins/auth/socks5.rb +22 -0
  159. data/lib/httpx/plugins/auth.rb +25 -0
  160. data/lib/httpx/plugins/aws_sdk_authentication.rb +111 -0
  161. data/lib/httpx/plugins/aws_sigv4.rb +239 -0
  162. data/lib/httpx/plugins/basic_auth.rb +29 -0
  163. data/lib/httpx/plugins/brotli.rb +50 -0
  164. data/lib/httpx/plugins/callbacks.rb +127 -0
  165. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  166. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  167. data/lib/httpx/plugins/circuit_breaker.rb +147 -0
  168. data/lib/httpx/plugins/content_digest.rb +204 -0
  169. data/lib/httpx/plugins/cookies/cookie.rb +174 -0
  170. data/lib/httpx/plugins/cookies/jar.rb +95 -0
  171. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +143 -0
  172. data/lib/httpx/plugins/cookies.rb +107 -0
  173. data/lib/httpx/plugins/digest_auth.rb +67 -0
  174. data/lib/httpx/plugins/expect.rb +120 -0
  175. data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
  176. data/lib/httpx/plugins/follow_redirects.rb +233 -0
  177. data/lib/httpx/plugins/grpc/call.rb +63 -0
  178. data/lib/httpx/plugins/grpc/grpc_encoding.rb +90 -0
  179. data/lib/httpx/plugins/grpc/message.rb +55 -0
  180. data/lib/httpx/plugins/grpc.rb +282 -0
  181. data/lib/httpx/plugins/h2c.rb +127 -0
  182. data/lib/httpx/plugins/internal_telemetry.rb +107 -0
  183. data/lib/httpx/plugins/ntlm_auth.rb +62 -0
  184. data/lib/httpx/plugins/oauth.rb +183 -0
  185. data/lib/httpx/plugins/persistent.rb +82 -0
  186. data/lib/httpx/plugins/proxy/http.rb +184 -0
  187. data/lib/httpx/plugins/proxy/socks4.rb +135 -0
  188. data/lib/httpx/plugins/proxy/socks5.rb +194 -0
  189. data/lib/httpx/plugins/proxy/ssh.rb +94 -0
  190. data/lib/httpx/plugins/proxy.rb +349 -0
  191. data/lib/httpx/plugins/push_promise.rb +81 -0
  192. data/lib/httpx/plugins/query.rb +35 -0
  193. data/lib/httpx/plugins/rate_limiter.rb +55 -0
  194. data/lib/httpx/plugins/response_cache/file_store.rb +140 -0
  195. data/lib/httpx/plugins/response_cache/store.rb +33 -0
  196. data/lib/httpx/plugins/response_cache.rb +333 -0
  197. data/lib/httpx/plugins/retries.rb +230 -0
  198. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  199. data/lib/httpx/plugins/stream.rb +183 -0
  200. data/lib/httpx/plugins/stream_bidi.rb +315 -0
  201. data/lib/httpx/plugins/upgrade/h2.rb +64 -0
  202. data/lib/httpx/plugins/upgrade.rb +86 -0
  203. data/lib/httpx/plugins/webdav.rb +86 -0
  204. data/lib/httpx/plugins/xml.rb +76 -0
  205. data/lib/httpx/pmatch_extensions.rb +33 -0
  206. data/lib/httpx/pool.rb +190 -0
  207. data/lib/httpx/punycode.rb +22 -0
  208. data/lib/httpx/request/body.rb +158 -0
  209. data/lib/httpx/request.rb +328 -0
  210. data/lib/httpx/resolver/entry.rb +30 -0
  211. data/lib/httpx/resolver/https.rb +256 -0
  212. data/lib/httpx/resolver/multi.rb +102 -0
  213. data/lib/httpx/resolver/native.rb +547 -0
  214. data/lib/httpx/resolver/resolver.rb +173 -0
  215. data/lib/httpx/resolver/system.rb +255 -0
  216. data/lib/httpx/resolver.rb +189 -0
  217. data/lib/httpx/response/body.rb +242 -0
  218. data/lib/httpx/response/buffer.rb +115 -0
  219. data/lib/httpx/response.rb +304 -0
  220. data/lib/httpx/selector.rb +282 -0
  221. data/lib/httpx/session.rb +612 -0
  222. data/lib/httpx/session_extensions.rb +30 -0
  223. data/lib/httpx/timers.rb +133 -0
  224. data/lib/httpx/transcoder/body.rb +43 -0
  225. data/lib/httpx/transcoder/chunker.rb +115 -0
  226. data/lib/httpx/transcoder/deflate.rb +37 -0
  227. data/lib/httpx/transcoder/form.rb +68 -0
  228. data/lib/httpx/transcoder/gzip.rb +71 -0
  229. data/lib/httpx/transcoder/json.rb +71 -0
  230. data/lib/httpx/transcoder/multipart/decoder.rb +141 -0
  231. data/lib/httpx/transcoder/multipart/encoder.rb +120 -0
  232. data/lib/httpx/transcoder/multipart/mime_type_detector.rb +78 -0
  233. data/lib/httpx/transcoder/multipart/part.rb +35 -0
  234. data/lib/httpx/transcoder/multipart.rb +31 -0
  235. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  236. data/lib/httpx/transcoder/utils/deflater.rb +75 -0
  237. data/lib/httpx/transcoder.rb +91 -0
  238. data/lib/httpx/utils.rb +75 -0
  239. data/lib/httpx/version.rb +5 -0
  240. data/lib/httpx.rb +66 -0
  241. data/sig/altsvc.rbs +33 -0
  242. data/sig/buffer.rbs +27 -0
  243. data/sig/callbacks.rbs +15 -0
  244. data/sig/chainable.rbs +55 -0
  245. data/sig/connection/http1.rbs +85 -0
  246. data/sig/connection/http2.rbs +116 -0
  247. data/sig/connection.rbs +169 -0
  248. data/sig/domain_name.rbs +17 -0
  249. data/sig/errors.rbs +69 -0
  250. data/sig/headers.rbs +49 -0
  251. data/sig/httpx.rbs +27 -0
  252. data/sig/io/ssl.rbs +27 -0
  253. data/sig/io/tcp.rbs +72 -0
  254. data/sig/io/udp.rbs +25 -0
  255. data/sig/io/unix.rbs +26 -0
  256. data/sig/io.rbs +3 -0
  257. data/sig/loggable.rbs +17 -0
  258. data/sig/options.rbs +202 -0
  259. data/sig/parser/http1.rbs +59 -0
  260. data/sig/plugins/auth/basic.rbs +17 -0
  261. data/sig/plugins/auth/digest.rbs +25 -0
  262. data/sig/plugins/auth/ntlm.rbs +20 -0
  263. data/sig/plugins/auth/socks5.rbs +18 -0
  264. data/sig/plugins/auth.rbs +13 -0
  265. data/sig/plugins/aws_sdk_authentication.rbs +43 -0
  266. data/sig/plugins/aws_sigv4.rbs +78 -0
  267. data/sig/plugins/basic_auth.rbs +15 -0
  268. data/sig/plugins/brotli.rbs +22 -0
  269. data/sig/plugins/callbacks.rbs +38 -0
  270. data/sig/plugins/circuit_breaker.rbs +71 -0
  271. data/sig/plugins/compression.rbs +57 -0
  272. data/sig/plugins/content_digest.rbs +51 -0
  273. data/sig/plugins/cookies/cookie.rbs +55 -0
  274. data/sig/plugins/cookies/jar.rbs +26 -0
  275. data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
  276. data/sig/plugins/cookies.rbs +28 -0
  277. data/sig/plugins/digest_auth.rbs +21 -0
  278. data/sig/plugins/expect.rbs +15 -0
  279. data/sig/plugins/fiber_concurrency.rbs +51 -0
  280. data/sig/plugins/follow_redirects.rbs +47 -0
  281. data/sig/plugins/grpc/call.rbs +23 -0
  282. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  283. data/sig/plugins/grpc/message.rbs +17 -0
  284. data/sig/plugins/grpc.rbs +65 -0
  285. data/sig/plugins/h2c.rbs +27 -0
  286. data/sig/plugins/ntlm_auth.rbs +21 -0
  287. data/sig/plugins/oauth.rbs +68 -0
  288. data/sig/plugins/persistent.rbs +14 -0
  289. data/sig/plugins/proxy/http.rbs +30 -0
  290. data/sig/plugins/proxy/socks4.rbs +37 -0
  291. data/sig/plugins/proxy/socks5.rbs +49 -0
  292. data/sig/plugins/proxy/ssh.rbs +18 -0
  293. data/sig/plugins/proxy.rbs +70 -0
  294. data/sig/plugins/push_promise.rbs +23 -0
  295. data/sig/plugins/query.rbs +18 -0
  296. data/sig/plugins/rate_limiter.rbs +13 -0
  297. data/sig/plugins/response_cache/file_store.rbs +19 -0
  298. data/sig/plugins/response_cache/store.rbs +13 -0
  299. data/sig/plugins/response_cache.rbs +86 -0
  300. data/sig/plugins/retries.rbs +66 -0
  301. data/sig/plugins/ssrf_filter.rbs +26 -0
  302. data/sig/plugins/stream.rbs +54 -0
  303. data/sig/plugins/stream_bidi.rbs +68 -0
  304. data/sig/plugins/upgrade/h2.rbs +9 -0
  305. data/sig/plugins/upgrade.rbs +29 -0
  306. data/sig/plugins/webdav.rbs +23 -0
  307. data/sig/plugins/xml.rbs +37 -0
  308. data/sig/pool.rbs +51 -0
  309. data/sig/punycode.rbs +5 -0
  310. data/sig/request/body.rbs +34 -0
  311. data/sig/request.rbs +88 -0
  312. data/sig/resolver/entry.rbs +13 -0
  313. data/sig/resolver/https.rbs +45 -0
  314. data/sig/resolver/multi.rbs +32 -0
  315. data/sig/resolver/native.rbs +74 -0
  316. data/sig/resolver/resolver.rbs +64 -0
  317. data/sig/resolver/system.rbs +34 -0
  318. data/sig/resolver.rbs +48 -0
  319. data/sig/response/body.rbs +52 -0
  320. data/sig/response/buffer.rbs +23 -0
  321. data/sig/response.rbs +103 -0
  322. data/sig/selector.rbs +68 -0
  323. data/sig/session.rbs +104 -0
  324. data/sig/timers.rbs +54 -0
  325. data/sig/transcoder/body.rbs +24 -0
  326. data/sig/transcoder/chunker.rbs +49 -0
  327. data/sig/transcoder/deflate.rbs +12 -0
  328. data/sig/transcoder/form.rbs +34 -0
  329. data/sig/transcoder/gzip.rbs +27 -0
  330. data/sig/transcoder/json.rbs +28 -0
  331. data/sig/transcoder/multipart.rbs +103 -0
  332. data/sig/transcoder/utils/body_reader.rbs +15 -0
  333. data/sig/transcoder/utils/deflater.rbs +28 -0
  334. data/sig/transcoder.rbs +43 -0
  335. data/sig/utils.rbs +19 -0
  336. metadata +518 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for using the experimental QUERY HTTP method
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/Query
9
+ module Query
10
+ def self.subplugins
11
+ {
12
+ retries: QueryRetries,
13
+ }
14
+ end
15
+
16
+ module InstanceMethods
17
+ def query(*uri, **options)
18
+ request("QUERY", uri, **options)
19
+ end
20
+ end
21
+
22
+ module QueryRetries
23
+ module InstanceMethods
24
+ private
25
+
26
+ def repeatable_request?(request, options)
27
+ super || request.verb == "QUERY"
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ register_plugin :query, Query
34
+ end
35
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for retrying requests when the request:
7
+ #
8
+ # * is rate limited;
9
+ # * when the server is unavailable (503);
10
+ # * when a 3xx request comes with a "retry-after" value
11
+ #
12
+ # https://gitlab.com/os85/httpx/wikis/Rate-Limiter
13
+ #
14
+ module RateLimiter
15
+ class << self
16
+ RATE_LIMIT_CODES = [429, 503].freeze
17
+
18
+ def configure(klass)
19
+ klass.plugin(:retries,
20
+ retry_change_requests: true,
21
+ retry_on: method(:retry_on_rate_limited_response),
22
+ retry_after: method(:retry_after_rate_limit))
23
+ end
24
+
25
+ def retry_on_rate_limited_response(response)
26
+ return false unless response.is_a?(Response)
27
+
28
+ status = response.status
29
+
30
+ RATE_LIMIT_CODES.include?(status)
31
+ end
32
+
33
+ # Servers send the "Retry-After" header field to indicate how long the
34
+ # user agent ought to wait before making a follow-up request. When
35
+ # sent with a 503 (Service Unavailable) response, Retry-After indicates
36
+ # how long the service is expected to be unavailable to the client.
37
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
38
+ # the minimum time that the user agent is asked to wait before issuing
39
+ # the redirected request.
40
+ #
41
+ def retry_after_rate_limit(_, response)
42
+ return unless response.is_a?(Response)
43
+
44
+ retry_after = response.headers["retry-after"]
45
+
46
+ return unless retry_after
47
+
48
+ Utils.parse_retry_after(retry_after)
49
+ end
50
+ end
51
+ end
52
+
53
+ register_plugin :rate_limiter, RateLimiter
54
+ end
55
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module HTTPX::Plugins
6
+ module ResponseCache
7
+ # Implementation of a file system based cache store.
8
+ #
9
+ # It stores cached responses in a file under a directory pointed by the +dir+
10
+ # variable (defaults to the default temp directory from the OS), in a custom
11
+ # format (similar but different from HTTP/1.1 request/response framing).
12
+ class FileStore
13
+ CRLF = HTTPX::Connection::HTTP1::CRLF
14
+
15
+ attr_reader :dir
16
+
17
+ def initialize(dir = Dir.tmpdir)
18
+ @dir = Pathname.new(dir).join("httpx-response-cache")
19
+
20
+ FileUtils.mkdir_p(@dir)
21
+ end
22
+
23
+ def clear
24
+ FileUtils.rm_rf(@dir)
25
+ end
26
+
27
+ def get(request)
28
+ path = file_path(request)
29
+
30
+ return unless File.exist?(path)
31
+
32
+ File.open(path, mode: File::RDONLY | File::BINARY) do |f|
33
+ f.flock(File::Constants::LOCK_SH)
34
+
35
+ read_from_file(request, f)
36
+ end
37
+ end
38
+
39
+ def set(request, response)
40
+ path = file_path(request)
41
+
42
+ file_exists = File.exist?(path)
43
+
44
+ mode = file_exists ? File::RDWR : File::CREAT | File::Constants::WRONLY
45
+
46
+ File.open(path, mode: mode | File::BINARY) do |f|
47
+ f.flock(File::Constants::LOCK_EX)
48
+
49
+ if file_exists
50
+ cached_response = read_from_file(request, f)
51
+
52
+ if cached_response
53
+ next if cached_response == request.cached_response
54
+
55
+ cached_response.close
56
+
57
+ f.truncate(0)
58
+
59
+ f.rewind
60
+ end
61
+ end
62
+ # cache the request headers
63
+ f << request.verb << CRLF
64
+ f << request.uri << CRLF
65
+
66
+ request.headers.each do |field, value|
67
+ f << field << ":" << value << CRLF
68
+ end
69
+
70
+ f << CRLF
71
+
72
+ # cache the response
73
+ f << response.status << CRLF
74
+ f << response.version << CRLF
75
+
76
+ response.headers.each do |field, value|
77
+ f << field << ":" << value << CRLF
78
+ end
79
+
80
+ f << CRLF
81
+
82
+ response.body.rewind
83
+
84
+ IO.copy_stream(response.body, f)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def file_path(request)
91
+ @dir.join(request.response_cache_key)
92
+ end
93
+
94
+ def read_from_file(request, f)
95
+ # if it's an empty file
96
+ return if f.eof?
97
+
98
+ # read request data
99
+ verb = f.readline.delete_suffix!(CRLF)
100
+ uri = f.readline.delete_suffix!(CRLF)
101
+
102
+ request_headers = {}
103
+ while (line = f.readline) != CRLF
104
+ line.delete_suffix!(CRLF)
105
+ sep_index = line.index(":")
106
+
107
+ field = line.byteslice(0..(sep_index - 1))
108
+ value = line.byteslice((sep_index + 1)..-1)
109
+
110
+ request_headers[field] = value
111
+ end
112
+
113
+ status = f.readline.delete_suffix!(CRLF)
114
+ version = f.readline.delete_suffix!(CRLF)
115
+
116
+ response_headers = {}
117
+ while (line = f.readline) != CRLF
118
+ line.delete_suffix!(CRLF)
119
+ sep_index = line.index(":")
120
+
121
+ field = line.byteslice(0..(sep_index - 1))
122
+ value = line.byteslice((sep_index + 1)..-1)
123
+
124
+ response_headers[field] = value
125
+ end
126
+
127
+ original_request = request.options.request_class.new(verb, uri, request.options)
128
+ original_request.merge_headers(request_headers)
129
+
130
+ response = request.options.response_class.new(request, status, version, response_headers)
131
+ response.original_request = original_request
132
+ response.finish!
133
+
134
+ IO.copy_stream(f, response.body)
135
+
136
+ response
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX::Plugins
4
+ module ResponseCache
5
+ # Implementation of a thread-safe in-memory cache store.
6
+ class Store
7
+ def initialize
8
+ @store = {}
9
+ @store_mutex = Thread::Mutex.new
10
+ end
11
+
12
+ def clear
13
+ @store_mutex.synchronize { @store.clear }
14
+ end
15
+
16
+ def get(request)
17
+ @store_mutex.synchronize do
18
+ @store[request.response_cache_key]
19
+ end
20
+ end
21
+
22
+ def set(request, response)
23
+ @store_mutex.synchronize do
24
+ cached_response = @store[request.response_cache_key]
25
+
26
+ cached_response.close if cached_response
27
+
28
+ @store[request.response_cache_key] = response
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for retrying requests when certain errors happen.
7
+ #
8
+ # https://gitlab.com/os85/httpx/wikis/Response-Cache
9
+ #
10
+ module ResponseCache
11
+ CACHEABLE_VERBS = %w[GET HEAD].freeze
12
+ CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
13
+ SUPPORTED_VARY_HEADERS = %w[accept accept-encoding accept-language cookie origin].sort.freeze
14
+ private_constant :CACHEABLE_VERBS
15
+ private_constant :CACHEABLE_STATUS_CODES
16
+
17
+ class << self
18
+ def load_dependencies(*)
19
+ require_relative "response_cache/store"
20
+ require_relative "response_cache/file_store"
21
+ end
22
+
23
+ # whether the +response+ can be stored in the response cache.
24
+ # (i.e. has a cacheable body, does not contain directives prohibiting storage, etc...)
25
+ def cacheable_response?(response)
26
+ response.is_a?(Response) &&
27
+ (
28
+ response.cache_control.nil? ||
29
+ # TODO: !response.cache_control.include?("private") && is shared cache
30
+ !response.cache_control.include?("no-store")
31
+ ) &&
32
+ CACHEABLE_STATUS_CODES.include?(response.status) &&
33
+ # RFC 2616 13.4 - A response received with a status code of 200, 203, 206, 300, 301 or
34
+ # 410 MAY be stored by a cache and used in reply to a subsequent
35
+ # request, subject to the expiration mechanism, unless a cache-control
36
+ # directive prohibits caching. However, a cache that does not support
37
+ # the Range and Content-Range headers MUST NOT cache 206 (Partial
38
+ # Content) responses.
39
+ response.status != 206
40
+ end
41
+
42
+ # whether the +response+
43
+ def not_modified?(response)
44
+ response.is_a?(Response) && response.status == 304
45
+ end
46
+
47
+ def extra_options(options)
48
+ options.merge(
49
+ supported_vary_headers: SUPPORTED_VARY_HEADERS,
50
+ response_cache_store: :store,
51
+ )
52
+ end
53
+ end
54
+
55
+ # adds support for the following options:
56
+ #
57
+ # :supported_vary_headers :: array of header values that will be considered for a "vary" header based cache validation
58
+ # (defaults to {SUPPORTED_VARY_HEADERS}).
59
+ # :response_cache_store :: object where cached responses are fetch from or stored in; defaults to <tt>:store</tt> (in-memory
60
+ # cache), can be set to <tt>:file_store</tt> (file system cache store) as well, or any object which
61
+ # abides by the Cache Store Interface
62
+ #
63
+ # The Cache Store Interface requires implementation of the following methods:
64
+ #
65
+ # * +#get(request) -> response or nil+
66
+ # * +#set(request, response) -> void+
67
+ # * +#clear() -> void+)
68
+ #
69
+ module OptionsMethods
70
+ private
71
+
72
+ def option_response_cache_store(value)
73
+ case value
74
+ when :store
75
+ Store.new
76
+ when :file_store
77
+ FileStore.new
78
+ else
79
+ value
80
+ end
81
+ end
82
+
83
+ def option_supported_vary_headers(value)
84
+ Array(value).sort
85
+ end
86
+ end
87
+
88
+ module InstanceMethods
89
+ # wipes out all cached responses from the cache store.
90
+ def clear_response_cache
91
+ @options.response_cache_store.clear
92
+ end
93
+
94
+ def build_request(*)
95
+ request = super
96
+ return request unless cacheable_request?(request)
97
+
98
+ prepare_cache(request)
99
+
100
+ request
101
+ end
102
+
103
+ private
104
+
105
+ def send_request(request, *)
106
+ return request if request.response
107
+
108
+ super
109
+ end
110
+
111
+ def fetch_response(request, *)
112
+ response = super
113
+
114
+ return unless response
115
+
116
+ if ResponseCache.not_modified?(response)
117
+ log { "returning cached response for #{request.uri}" }
118
+
119
+ response.copy_from_cached!
120
+ elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
121
+ request.options.response_cache_store.set(request, response) unless response.cached?
122
+ end
123
+
124
+ response
125
+ end
126
+
127
+ # will either assign a still-fresh cached response to +request+, or set up its HTTP
128
+ # cache invalidation headers in case it's not fresh anymore.
129
+ def prepare_cache(request)
130
+ cached_response = request.options.response_cache_store.get(request)
131
+
132
+ return unless cached_response && match_by_vary?(request, cached_response)
133
+
134
+ cached_response.body.rewind
135
+
136
+ if cached_response.fresh?
137
+ cached_response = cached_response.dup
138
+ cached_response.mark_as_cached!
139
+ request.response = cached_response
140
+ request.emit(:response, cached_response)
141
+ return
142
+ end
143
+
144
+ request.cached_response = cached_response
145
+
146
+ if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
147
+ request.headers.add("if-modified-since", last_modified)
148
+ end
149
+
150
+ if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
151
+ request.headers.add("if-none-match", etag)
152
+ end
153
+ end
154
+
155
+ def cacheable_request?(request)
156
+ request.cacheable_verb? &&
157
+ (
158
+ !request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
159
+ )
160
+ end
161
+
162
+ # whether the +response+ complies with the directives set by the +request+ "vary" header
163
+ # (true when none is available).
164
+ def match_by_vary?(request, response)
165
+ vary = response.vary
166
+
167
+ return true unless vary
168
+
169
+ original_request = response.original_request
170
+
171
+ if vary == %w[*]
172
+ request.options.supported_vary_headers.each do |field|
173
+ return false unless request.headers[field] == original_request.headers[field]
174
+ end
175
+
176
+ return true
177
+ end
178
+
179
+ vary.all? do |field|
180
+ !original_request.headers.key?(field) || request.headers[field] == original_request.headers[field]
181
+ end
182
+ end
183
+ end
184
+
185
+ module RequestMethods
186
+ # points to a previously cached Response corresponding to this request.
187
+ attr_accessor :cached_response
188
+
189
+ def initialize(*)
190
+ super
191
+ @cached_response = nil
192
+ end
193
+
194
+ def merge_headers(*)
195
+ super
196
+ @response_cache_key = nil
197
+ end
198
+
199
+ # returns whether this request is cacheable as per HTTP caching rules.
200
+ def cacheable_verb?
201
+ CACHEABLE_VERBS.include?(@verb)
202
+ end
203
+
204
+ # returns a unique cache key as a String identifying this request
205
+ def response_cache_key
206
+ @response_cache_key ||= begin
207
+ keys = [@verb, @uri]
208
+
209
+ @options.supported_vary_headers.each do |field|
210
+ value = @headers[field]
211
+
212
+ keys << value if value
213
+ end
214
+ Digest::SHA1.hexdigest("httpx-response-cache-#{keys.join("-")}")
215
+ end
216
+ end
217
+ end
218
+
219
+ module ResponseMethods
220
+ attr_writer :original_request
221
+
222
+ def initialize(*)
223
+ super
224
+ @cached = false
225
+ end
226
+
227
+ # a copy of the request this response was originally cached from
228
+ def original_request
229
+ @original_request || @request
230
+ end
231
+
232
+ # whether this Response was duplicated from a previously {RequestMethods#cached_response}.
233
+ def cached?
234
+ @cached
235
+ end
236
+
237
+ # sets this Response as being duplicated from a previously cached response.
238
+ def mark_as_cached!
239
+ @cached = true
240
+ end
241
+
242
+ # eager-copies the response headers and body from {RequestMethods#cached_response}.
243
+ def copy_from_cached!
244
+ cached_response = @request.cached_response
245
+
246
+ return unless cached_response
247
+
248
+ # 304 responses do not have content-type, which are needed for decoding.
249
+ @headers = @headers.class.new(cached_response.headers.merge(@headers))
250
+
251
+ @body = cached_response.body.dup
252
+
253
+ @body.rewind
254
+ end
255
+
256
+ # A response is fresh if its age has not yet exceeded its freshness lifetime.
257
+ # other (#cache_control} directives may influence the outcome, as per the rules
258
+ # from the {rfc}[https://www.rfc-editor.org/rfc/rfc7234]
259
+ def fresh?
260
+ if cache_control
261
+ return false if cache_control.include?("no-cache")
262
+
263
+ return true if cache_control.include?("immutable")
264
+
265
+ # check age: max-age
266
+ max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
267
+
268
+ max_age ||= cache_control.find { |directive| directive.start_with?("max-age") }
269
+
270
+ max_age = max_age[/age=(\d+)/, 1] if max_age
271
+
272
+ max_age = max_age.to_i if max_age
273
+
274
+ return max_age > age if max_age
275
+ end
276
+
277
+ # check age: expires
278
+ if @headers.key?("expires")
279
+ begin
280
+ expires = Time.httpdate(@headers["expires"])
281
+ rescue ArgumentError
282
+ return false
283
+ end
284
+
285
+ return (expires - Time.now).to_i.positive?
286
+ end
287
+
288
+ false
289
+ end
290
+
291
+ # returns the "cache-control" directives as an Array of String(s).
292
+ def cache_control
293
+ return @cache_control if defined?(@cache_control)
294
+
295
+ @cache_control = begin
296
+ return unless @headers.key?("cache-control")
297
+
298
+ @headers["cache-control"].split(/ *, */)
299
+ end
300
+ end
301
+
302
+ # returns the "vary" header value as an Array of (String) headers.
303
+ def vary
304
+ return @vary if defined?(@vary)
305
+
306
+ @vary = begin
307
+ return unless @headers.key?("vary")
308
+
309
+ @headers["vary"].split(/ *, */).map(&:downcase)
310
+ end
311
+ end
312
+
313
+ private
314
+
315
+ # returns the value of the "age" header as an Integer (time since epoch).
316
+ # if no "age" of header exists, it returns the number of seconds since {#date}.
317
+ def age
318
+ return @headers["age"].to_i if @headers.key?("age")
319
+
320
+ (Time.now - date).to_i
321
+ end
322
+
323
+ # returns the value of the "date" header as a Time object
324
+ def date
325
+ @date ||= Time.httpdate(@headers["date"])
326
+ rescue NoMethodError, ArgumentError
327
+ Time.now
328
+ end
329
+ end
330
+ end
331
+ register_plugin :response_cache, ResponseCache
332
+ end
333
+ end