# Axios Cache Interceptor https://axios-cache-interceptor.js.org/llms.txt ## Comparison > This comparison page aims to be detailed, unbiased, and up-to-date.
If you see any > information that may be inaccurate or could be improved otherwise, please feel free to > suggest changes. ### Cache Features ``` ✅ Supported 1st-class and documented. 🔶 Supported and documented, but requires custom user-code to implement. 🟡 Can be done, may not be documented. 🛑 Not officially supported or documented. ``` | | Axios Cache Interceptor | Axios Cache Adapter | Cachios | | :--------------------------------------------------------------------------: | :---------------------: | :--------------------------------------------------------------------: | :------------------: | | Compared version | Latest | 2.7.3 | 3.1.1 | | Expiration with [TTL](https://developer.mozilla.org/en-US/docs/Glossary/TTL) | ✅ | ✅ | ✅ | | Per-request configuration | ✅ | ✅ | ✅ | | Global and custom instance | ✅ | ✅ | ✅ | | Cache-Control header | ✅ | ✅ | 🛑 | | Expires & Age header | ✅ | 🟡 | 🛑 | | ETag and If-None-Match header | ✅ | 🛑 | 🛑 | | If-Modified-Size header | ✅ | 🛑 | 🛑 | | Bundle size | **4.4Kb** (gzip) | 18.9Kb (gzip) | 19.5Kb (gzip) | | Typescript declaration | ✅ (Custom interface) | ✅ (Applied globally) | ✅(Applied globally) | | Custom cache keys | ✅ | ✅ | ✅ | | Multiple storages | ✅ | 🔶 (Only localForage) | ✅ | | Built-in redis storage | 🔶 | ✅ | 🟡 | | Handles storage quota errors | ✅ | ✅ | ✅ | | Node & Web compatible | ✅ | ✅ | 🛑 | | Invalidate cache based on response | ✅ | ✅ | 🛑 | | Update cache based on response | ✅ | 🟡 | 🟡 | | Predicate to test if request should be cached | ✅ | ✅ | 🛑 | | Concurrent requests | ✅ | 🔶[#231](https://github.com/RasCarlito/axios-cache-adapter/issues/231) | 🛑 | | Cache fallback on network errors | ✅ | ✅ | ✅ | | Debug / Development mode | ✅ | ✅ | 🛑 | ### Benchmark The [benchmark](https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/benchmark/index.js) is composed of axios throughput tests to compare the performance of this library, `axios-cache-adapter` and axios as it is. <<< @/generated/benchmark.md {5,11} --- ## Debugging I'm certainly sure that along the way you will find some cache behavior that is not the expected to the current situation. To help with that, the library has a separate robust build with support to debug logs enabled. You can use it by changing the `setupCache` import: ::: code-group ```ts [EcmaScript] import Axios from 'axios'; // Only import from `/dev` where you import `setupCache`. import { setupCache } from 'axios-cache-interceptor'; // [!code --] import { setupCache } from 'axios-cache-interceptor/dev'; // [!code ++] // same object, but with updated typings. const axios = setupCache(Axios, { debug: console.log // [!code ++] }); ``` ```ts [Common JS] const Axios = require('axios'); // Only import from `/dev` where you import `setupCache`. const { setupCache } = require('axios-cache-interceptor'); // [!code --] const { setupCache } = require('axios-cache-interceptor/dev'); // [!code ++] // same object, but with updated typings. const axios = setupCache(Axios, { debug: console.log // [!code ++] }); ``` ```ts{3,4} [Browser] const Axios = window.axios; // Choose development bundle. // [!code ++] const { setupCache } = window.AxiosCacheInterceptor; // same object, but with updated typings. const axios = setupCache(Axios, { debug: console.log // [!code ++] }); ``` ```ts {5,11} [Skypack] import Axios from 'https://cdn.skypack.dev/axios'; // Only import from `/dev` where you import `setupCache`. import { setupCache } from 'https://cdn.skypack.dev/axios-cache-interceptor'; // [!code --] import { setupCache } from 'https://cdn.skypack.dev/axios-cache-interceptor/dev'; // [!code ++] // same object, but with updated typings. const axios = setupCache(Axios, { debug: console.log // [!code ++] }); ``` ::: And much more, depending on your context, situation and configuration. **Any misbehavior that you find will have a log to explain it.** ::: details Sample of logs sent to console. ```json [ { "id": "-644704205", "msg": "Sending request, waiting …", "data": { "overrideCache": false, "state": "empty" } }, { "id": "-644704205", "msg": "Waiting list had an deferred for this key, waiting for it to finish" }, { "id": "-644704205", "msg": "Detected concurrent request, waiting for it to finish" }, { "id": "-644704205", "msg": "Useful response configuration found", "data": { "cacheConfig": { /*...*/ }, "cacheResponse": { "data": { /*...*/ }, "status": 200, "statusText": "OK", "headers": { /*...*/ } } } }, { "id": "-644704205", "msg": "Found waiting deferred(s) and resolved them" }, { "id": "-644704205", "msg": "Returning cached response" }, // First request ended, second call below: { "id": "-644704205", "msg": "Response cached", "data": { "cache": { /*...*/ }, "response": { /*...*/ } } }, { "id": "-644704205", "msg": "Returning cached response" } ] ``` ::: --- ## Getting Started [Looking for axios v0?](https://axios-cache-interceptor.js.org/v0/) ### Install Axios Cache Interceptor Add Axios Cache Interceptor and Axios to your project using your favorite package manager: ::: code-group ```bash [NPM] npm install axios@^1 axios-cache-interceptor@^1 ``` ```html [Browser] ``` ```ts [Skypack] import Axios from 'https://cdn.skypack.dev/axios'; import { setupCache } from 'https://cdn.skypack.dev/axios-cache-interceptor'; ``` ::: ### Setup Axios Cache Interceptor After installing, you can import the package and apply the interceptor to your axios instance, as shown below: ::: code-group ```ts [EcmaScript] import Axios from 'axios'; import { setupCache } from 'axios-cache-interceptor'; const instance = Axios.create(); // [!code focus] const axios = setupCache(instance); // [!code focus] const req1 = axios.get('https://api.example.com/'); // [!code focus] const req2 = axios.get('https://api.example.com/'); // [!code focus] const [res1, res2] = await Promise.all([req1, req2]); res1.cached; // false // [!code focus] res2.cached; // true // [!code focus] ``` ```ts [CommonJS] const Axios = require('axios'); const { setupCache } = require('axios-cache-interceptor'); const instance = Axios.create(); // [!code focus] const axios = setupCache(instance); // [!code focus] const req1 = axios.get('https://api.example.com/'); // [!code focus] const req2 = axios.get('https://api.example.com/'); // [!code focus] const [res1, res2] = await Promise.all([req1, req2]); res1.cached; // false // [!code focus] res2.cached; // true // [!code focus] ``` ```ts [Browser] const Axios = window.axios; const { setupCache } = window.AxiosCacheInterceptor; const instance = Axios.create(); // [!code focus] const axios = setupCache(instance); // [!code focus] const req1 = axios.get('https://api.example.com/'); // [!code focus] const req2 = axios.get('https://api.example.com/'); // [!code focus] const [res1, res2] = await Promise.all([req1, req2]); res1.cached; // false // [!code focus] res2.cached; // true // [!code focus] ``` ```ts [Skypack] import Axios from 'https://cdn.skypack.dev/axios'; import { setupCache } from 'https://cdn.skypack.dev/axios-cache-interceptor'; const instance = Axios.create(); // [!code focus] const axios = setupCache(instance); // [!code focus] const req1 = axios.get('https://api.example.com/'); // [!code focus] const req2 = axios.get('https://api.example.com/'); // [!code focus] const [res1, res2] = await Promise.all([req1, req2]); res1.cached; // false // [!code focus] res2.cached; // true // [!code focus] ``` ::: Just the above is sufficient for most use cases. However, you can also customize each cache behavior by passing a configuration object to the `setupCache` function. And you can also customize some behaviors each request by using the `cache` option in the request config. ### Support Table Most of axios v0 breaking changes were about typing issues, so your version may work with one outside of this table. **Axios and Axios Cache Interceptor v0 are not compatible with Axios and Axios Cache Interceptor v1** > **Note**: Axios was not defined as a `peerDependency` for all v0 versions, because it > had a non-stable semver version. > [See #145 (Comment)](https://github.com/arthurfiorette/axios-cache-interceptor/issues/145#issuecomment-1042710481) | [Axios](https://github.com/axios/axios/releases) | [Axios Cache Interceptor](https://github.com/arthurfiorette/axios-cache-interceptor/releases) | | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | | `>= v1.7.8` | `>= v1.7.0` | | `>= v1.6` | `>= v1.3.0 && <= 1.6.2` | | `>= v1.4` | `>= v1.2.0` | | `>= v1.3.1` | `>= v1` | | `>= v0.27` | `>= v0.10.3` | | `>= v0.26` | `>= v0.8.4` | | `~ v0.25` | `~ v0.8.4` | | `~ v0.24` | `>= v0.5 && <= 0.8.3` | | `~ v0.23` | `~ v0.4` | | `~ v0.22` | `~ v0.3` | | `v0.21` | `<= v0.2` | #### Read More Some useful links to get you more familiar with the library: - [Debugging requests](./debugging.md) - [Storages](./storages.md) - [Global config](../config.md) - [Per request config](../config/request-specifics.md) - [Response object](../config/response-object.md) --- ## Introduction Axios Cache Interceptor is a, as it name says, a interceptor for axios to handle caching. It was created to help developers call axios multiple times without having to worry about overloading the network or coding himself a simple and buggy cache system. Each request goes through an interceptor applied to your axios instance. There, we handle each request and decide if we should send it to the network or return a cached response. ### How it works By using axios interceptors instead of adapters, each request is passed through the interceptor before calling the adapter and before returning to the original caller. Adapters are the final step and they are responsible for the actual network call, so, by choosing to use interceptors, we create a minimally invasive approach that allows you to still use the axios adapter of your choice. Before the request is delivered to the adapter, our request interceptor checks if the request have already been cached and if it's a valid one, checks if the request should be cached (sometimes you don't want cache at all, and it's ok), if there's already a request sent to the network that we can wait for it and many more other checks. After the adapter gets the response, we check if it belongs to a _cacheable_ request, saves it into the storage, resolves other requests awaiting for the same resource and finally returns the response to the original caller. ### Features - TTL, Cache-Control and ETag. - Return previous cached request if the new one failed. - Handles parallel requests - 100% Customizable - Built-in storages like In-Memory, Local Storage and Session Storage. - Less than 4.3Kb minified and gzipped. - Development mode to debug your requests. - 22 times faster than using axios and 8% faster than `axios-cache-adapter`. - And much more... ### Why not...? #### axios-cache-adapter The creation of this library is heavily inspired by axios-cache-adapter. It was a great library but now it is unmaintained and has a lot of unresolved issues. Also, it weights more than 4x the size of this library with less features and less performance. #### Fetch and some state management library? As this library was built to be used with axios and to handle storage itself, I can assure that it is more performant that any glued code you may find and/or write yourself. About state management libraries and other similar things, [this blog post](https://arthur.place/implications-of-cache-or-state) explains why cache is more correct, architectural way, instead of state. --- ## Invalidating Cache When using cache-first approaches to improve performance, data inconsistency becomes your major problem. That occurs because **you** can mutate data in the server and **others** also can too. It becomes impossible to really know what the current state of the data is in real time without communicating with the server. ::: warning **All available revalidation methods only work when the request is successful.** If you are wanting to revalidate with a non standard `2XX` status code, make sure to enable it at [`validateStatus`](https://axios-http.com/docs/handling_errors) or revalidate it manually as shown [below](#updating-cache-through-external-sources). ::: Take a look at this simple example: 1. User lists all available posts, the server returns an empty array. 2. User proceeds to create a new post, server returns 200 OK. 3. Your frontend navigates to the post list page. 4. The post list page still shows 0 posts because it had a recent cache for that request. 5. Your client shows 0 posts, but the server actually has 1 post. ### Revalidation after mutation In most cases, you are the one responsible for that inconsistency, like in the above example when the client itself initiated the mutation request. When that happens, you are capable of invalidating the cache for all places you have changed too. **The `cache.update` option is available for every request that you make, and it will be the go-to tool for invalidation.** ::: tip By centralizing your requests into separate methods, you are more likely to keep track of custom IDs you use for each request, thus making it easier to reference and invalidate after. ::: ### Programmatically If the mutation you made was just simple changes, you can get the mutation response and programmatically update your cache. Again considering the first example, we can just do an `array.push` to the `list-posts` cache and we are good to go. ```ts // Uses `list-posts` id to be able to reference it later. function listPosts() { return axios.get('/posts', { id: 'list-posts' }); } function createPost(data) { return axios.post( '/posts', data, /* [!code focus:25] */ { cache: { update: { // Will perform a cache update for the `list-posts` respective // cache entry. 'list-posts': (listPostsCache, createPostResponse) => { // If the cache doesn't have a cached state, we don't need // to update it if (listPostsCache.state !== 'cached') { return 'ignore'; } // Imagine the server response for the `list-posts` request // is: { posts: Post[]; }, and the `create-post` response // comes with the newly created post. // Adds the created post to the end of the post's list listPostsCache.data.posts.push(createPostResponse.data); // Return the same cache state, but a updated one. return listPostsCache; } } } } ); } ``` This will update the `list-posts` cache at the client side, making it equal to the server. When operations like this are possible to be made, they are the preferred. That's because we do not contact the server again and update ourselves the cache. ::: tip **Note to Vue users:** If you modify an array as shown above and then assign the result data of the axios request to a Vue `ref`, you may have issues with the UI not updating. This is because the cached array is the same object as was returned from the previous request. You need to copy the array before modifying it: ```ts listPostsCache.data.posts = [...listPostsCache.data.posts]; listPostsCache.data.posts.push(createPostResponse.data); // or listPostsCache.data.posts = [ ...listPostsCache.data.posts, createPostResponse.data ]; ``` or before assigning it to the `ref`: ```ts myRef.value = [...axios.get(url).data]; ``` ::: ### Through network Sometimes, the mutation you made is not simple enough and would need a lot of copied service code to replicate all changes the backend made, turning it into a duplication and maintenance nightmare. In those cases, you can just invalidate the cache and let the next request be forwarded to the server, and update the cache with the new network response. ```ts // Uses `list-posts` id to be able to reference it later. function listPosts() { return axios.get('/posts', { // [!code focus:3] id: 'list-posts' }); } function createPost(data) { return axios.post('/posts', data, { // [!code focus:9] cache: { update: { // Internally calls the storage.remove('list-posts') and lets the // next request be forwarded to the server without you having to // do any checks. 'list-posts': 'delete' } } }); } ``` Still using the first example, while we are at the step **3**, automatically, the axios cache-interceptor instance will request the server again and do required changes in the cache before the promise resolves and your page gets rendered. ### Through external sources If you have any other type of external communication, like when listening to a websocket for changes, you may want to update your axios cache without be in a request context. For that, you can operate the storage manually. It is simple as that: ```ts if (someLogicThatShowsIfTheCacheShouldBeInvalidated) { // Deletes the current cache for the `list-posts` respective request. await axios.storage.remove('list-posts'); } if (someLogicThatShowsIfTheCacheShouldBeInvalidated) { // Deletes all cached data await axios.storage.clear(); } ``` ### Keeping cache up to date If you were **not** the one responsible for that change, your client may not be aware that it has changed. E.g. When you are using a chat application, you may not be aware that a new message was sent to you. In such cases that we **do not** have a way to know that the cache is outdated, you may have to end up setting a custom time to live (TTL) for specific requests. ```ts // Uses `list-posts` id to be able to reference it later. function listPosts() { return axios.get('/posts', { id: 'list-posts', cache: { ttl: 1000 * 60 // 1 minute. } }); } function createPost(data) { return axios.post('/posts', data, { cache: { update: { // I still want to delete the cache when I KNOW things have // changed, but, by setting a TTL of 1 minute, I ensure that // 1 minute is the highest time interval that the cache MAY // get outdated. 'list-posts': 'delete' } } }); } ``` ### Summing up When applying any kind of cache to any kind of application, you chose to trade data consistency for performance. And, most of the time that is OK. _The best cache strategy is a combination of all of them. TTL, custom revalidation, stale while revalidate and all the others together are the best solution._ The only real tip here is to you put on a scale the amount of inconsistency you are willing to give up for the performance you are willing to gain. **Sometimes, not caching is the best solution.** --- --- layout: home hero: name: Axios Cache Interceptor text: Performant, small and powerful tagline: A cache interceptor for axios made with developers and performance in mind. image: src: /rocket.svg alt: Rocket title: Axios Cache Interceptor's logo actions: - theme: brand text: Get Started link: /guide/getting-started - theme: alt text: Why cache? link: https://arthur.place/implications-of-cache-or-state - theme: alt text: View on GitHub link: https://github.com/arthurfiorette/axios-cache-interceptor features: - icon: ⚡ title: Simply faster details: Serving 21x more requests/s than axios itself. - icon: 📦 title: Handy builds details: No matter what's you JS setup, we got you covered! CDN, EcmaScript, UMD, CommonJS and URL imports. - icon: 🔩 title: Hassle free details: Just setupCache() and watch the magic happen! Works for everyone, no matter the current combination of adapters or interceptors. - icon: 🛠️ title: Rich Features details: We follow strict rules defined by MDN, RFCs, and other specifications. No more guessing. - icon: 🌐 title: No network waste! details: Network speed should not matter for your users. Make your application work offline, on 2G or ultra-fast 5G, it's up to your users. - icon: 🔑 title: TypeScript! details: Fully configurable and flexible interceptors with full type-safe typing. --- --- ## Other Interceptors When combining `axios-cache-interceptors` with other interceptors, you may encounter some inconsistences. Which is explained in the next section. ### TL;DR - **Request** interceptors registered before `setupCache()` are ran before and registrations made after are ran after. - **Response** interceptors registered before `setupCache()` are ran **after** and registrations made after are ran **before**. ### Explanation Axios interceptors are ran differently for the request and response ones. - **Request interceptors** are FILO _(First In Last Out)_ - **Response interceptors** are FIFO _(First In First Out)_ As explained better in the [Axios documentation](https://github.com/axios/axios#interceptors) and in [this issue](https://github.com/arthurfiorette/axios-cache-interceptor/issues/449#issuecomment-1370327566). ```ts // This will be ran BEFORE the cache interceptor axios.interceptors.request.use((req) => req); // This will be ran AFTER the cache interceptor axios.interceptors.response.use((res) => res); setupCache(axios); // This will be ran AFTER the cache interceptor axios.interceptors.request.use((req) => req); // This will be ran BEFORE the cache interceptor axios.interceptors.response.use((res) => res); ``` --- ### Extending types When using axios-cache-interceptor, you'll note that it have a different type than the defaults `AxiosInstance`, `AxiosRequestConfig` and `AxiosResponse`. That's because was chosen to override axios's interfaces instead of extending, to avoid breaking changes with other libraries. However, this also means that when integrating with other packages or creating your own custom interceptor, you need to override/extend our own types, `CacheInstance`, `CacheRequestConfig` and `CacheAxiosResponse` to match your needs. This can be done as shown below: ```ts declare module 'axios-cache-interceptor' { interface CacheRequestConfig { customProperty: string; } } ``` ### Streams and non-JSON Sometimes you may want to cache a response that is not `JSON`, or that is a `Stream`. Either created by another interceptor or even by the axios adapter itself. To do so, you can use the axios's native `transformResponse` option, which is a function that receives the response and returns a string or a buffer. **Axios Cache Interceptor** can only handle serializable data types, so you need to convert the response to a string or a buffer. ```ts import Axios from 'axios'; import { setupCache } from 'axios-cache-interceptor'; const instance = Axios.create(); const axios = setupCache(instance); // [!code focus:8] const response = await axios.get('my-url-that-returns-a-stream', { responseType: 'stream', transformResponse(response) { // You will need to implement this function. return convertStreamToStringOrObject(response.data); } }); response.data; // Will be a string and will be able to be cached. ``` This library cannot handle streams or buffers, so if you still need `response.data` to be a stream or buffer, you will need to cache it manually. If you can collect the response data into a serializable format, `axios-cache-interceptor` can handle it for you with help of the `transformResponse` option. --- ## Request Id We can distinguish requests from each other by assigning an **non unique** `id` to each request. These IDs are the same provided to the storage as keys. Each ID is responsible for binding a cache to its request, for referencing or invalidating it later and to make the interceptor use the same cache for requests to the same endpoint and parameters. The default id generator is smart enough to generate the same ID for theoretically same requests. `{ baseURL: 'https://a.com/', url: '/b' }` **==** `{ url: 'https://a.com/b/' }`. ::: code-group ```ts [Different requests] import Axios from 'axios'; import { setupCache } from 'axios-cache-interceptor'; const axios = setupCache(Axios); // [!code focus:5] // These two requests are from completely different endpoints, but they will share // the same resources and cache, as both have the same ID. const reqA = await axios.get('/a', { id: 'custom-id' }); const reqB = await axios.get('/b', { id: 'custom-id' }); ``` ```ts [Different contexts] import Axios from 'axios'; import { setupCache } from 'axios-cache-interceptor'; const axios = setupCache(Axios); // [!code focus:7] // You can use the same logic to create two caches for the same endpoint. // Allows you to have different use cases for the coincident same endpoint. const userForPageX = await axios.get('/users', { id: 'users-page-x' }); const userForPageY = await axios.get('/users', { id: 'users-page-y' }); ``` ::: ::: warning If you send two different requests forcefully with the same ID. This library will ignore any possible differences between them and share the same cache for both. ::: ### Custom Generator By default, the id generator extracts `method`, `baseURL`, `query`, `params`, `data` and `url` properties from the request object and hashes it into a number with [`object-code`](https://www.npmjs.com/package/object-code). While this default implementation offers reasonable uniqueness for most scenarios, it's worth noting that there's a [theoretical 50% probability of collisions after approximately 77,000 keys](https://preshing.com/20110504/hash-collision-probabilities/) have been generated. However, this limitation is typically inconsequential in browser environments due to their 5MB storage limit, which is reached long before the collision threshold. ::: warning Consider implementing a custom key generator function using libraries like [`object-hash`](https://www.npmjs.com/package/object-hash) for generating hash keys with significantly lower collision probabilities when hitting over 77K unique keys is a possibility ::: Here's an example of a generator that only uses the `url` and `method` and `custom` properties: ```ts import Axios from 'axios'; import { setupCache, buildKeyGenerator } from 'axios-cache-interceptor'; const axios = setupCache(Axios, { generateKey: buildKeyGenerator((request /* [!code focus:5] */) => ({ method: request.method, url: request.url, custom: logicWith(request.method, request.url) })) }); ``` --- ## Request specifics Each request can have its own cache customization, by using the `cache` property. This way, you can have requests behaving differently from each other without much effort. The inline documentation is self explanatory, but here is a shortly brief of what each property does: ::: tip You can override every request specific property when creating the cached axios client, the same way you do with the [global options](../config.md). ::: ### id - Type: `string` - default: _(auto generated by the current [key generator](../guide/request-id.md#custom-generator))_ The [Request ID](../guide/request-id.md) used in this request. It may have been generated by the [Key Generator](../guide/request-id.md#custom-generator) or a custom one provided by [`config.id`](./request-specifics.md#id) ### cache - Type: `false` or `Partial>`. - Default: `{}` _(Inherits from global configuration)_ ::: tip As this property is optional, when not provided, all properties will inherit from global configuration ::: The cache option available through the request config is where all the cache customization happens. Setting the `cache` property to `false` will disable the cache for this request. This does not mean that the cache will be excluded from the storage, in which case, you can do that by deleting the storage entry: ```ts // Make a request with cache disabled. const { id: requestId } = await axios.get('url', { cache: false }); // Delete the cache entry for this request. await axios.storage.remove(requestId); ``` ### cache.ttl - Type: `number` - Default: `1000 * 60 * 5` _(5 Minutes)_ ::: warning When using [**interpretHeader**](#cache-interpretheader), this value will only be used if the interpreter can't determine their TTL value to override this one. ::: The time until the cached value is expired in milliseconds. If a function is used, it will receive the complete response and waits to return a TTL value ### cache.interpretHeader - Type: `boolean` - Default: `true` ::: tip You can override the default behavior by changing the **[headerInterpreter](../config.md#headerinterpreter)** option. ::: If activated, when the response is received, the `ttl` property will be inferred from the requests headers. As described in the MDN docs and HTML specification. See the actual implementation of the [`interpretHeader`](https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/src/header/interpreter.ts) method for more information. ### cache.cacheTakeover - Type: `boolean` - Default: `true` As most of our cache strategies depends on well known defined HTTP headers, most browsers also use those headers to define their own cache strategies and storages. ::: details This can be seen when opening network tab in your browser's dev tools. ![Network tab in Chrome Dev Tools](../assets/disk-cache-screenshot.png 'A network disk-cache example') ::: When your requested routes includes `Cache-Control` in their responses, you may end up with we and your browser caching the response, resulting in a **double layer of cache**. This option solves this by including some predefined headers in the request, that should tell any client / adapter to not cache the response, thus only we will cache it. **These are headers used in our specific request, it won't affect any other request or response that the server may handle.** Headers included: - `Cache-Control: no-cache` - `Pragma: no-cache` - `Expires: 0` ::: warning This option will not work on most **CORS** requests, as the browser will throw `Request header field pragma is not allowed by Access-Control-Allow-Headers in preflight response.`. When you encounter this error, you need to make sure `Cache-Control`, `Pragma` and `Expires` headers are included into your server's `Access-Control-Allow-Headers` CORS configuration. If you cannot do such thing, you can fallback to disabling this option. Learn more on why it should be enabled at [#437](https://github.com/arthurfiorette/axios-cache-interceptor/issues/437#issuecomment-1361262194) and in this [StackOverflow](https://stackoverflow.com/a/62781874/14681561) answer. ::: ### cache.methods - Type: `Method[]` - Default: `["get", "head"]` Specifies which methods we should handle and cache. This is where you can enable caching to `POST`, `PUT`, `DELETE` and other methods, as the default is only `GET`. If you want to enable cache for `POST` requests, you can do: ```ts // Globally enables caching for POST requests const axios = setupCache(instance, { methods: ['get', 'post'] }); // Just for this request axios.post('url', data, { cache: { methods: ['post'] } }); ``` We use `methods` in a per-request configuration setup because sometimes you have exceptions to the method rule. ### cache.cachePredicate - Type: `CachePredicate` - Default: `{ statusCheck: (status) => [200, 203, 300, 301, 302, 404, 405, 410, 414, 501].includes(status) }` _(These default status codes follows RFC 7231)_ An object or function that will be tested against the response to indicate if it can be cached. You can use `statusCheck`, `containsHeader`, `ignoreUrls` and `responseMatch` to test against the response. ```ts{5,8,13} axios.get<{ auth: { status: string } }>('url', { cache: { cachePredicate: { // Only cache if the response comes with a "good" status code statusCheck: (status) => true, // some calculation // Tests against any header present in the response. containsHeaders: { 'x-custom-header-3': (value) => true // some calculation }, // Check custom response body responseMatch: ({ data }) => { // Sample that only caches if the response is authenticated return data.auth.status === 'authenticated'; }, // Ensures no request is cached if its url starts with "/api" ignoreUrls: [/^\/api/] } } }); ``` ### cache.update - Type: `CacheUpdater` - Default: `{}` Once the request is resolved, this specifies what other responses should change their cache. Can be used to update the request or delete other caches. It is a simple `Record` with the request id. Here's an example with some basic login: Using a function instead of an object is supported but not recommended, as it's better to just consume the response normally and write your own code after it. But it`s here in case you need it. ```ts // Some requests id's let profileInfoId; let userInfoId; axios.post<{ auth: { user: User } }>( 'login', { username, password }, { cache: { update: { // Evicts the profile info cache, because now he is authenticated and the response needs to be re-fetched [profileInfoId]: 'delete', // An example that update the "user info response cache" when doing a login. // Imagine this request is a login one. [userInfoResponseId]: (cachedValue, response) => { if (cachedValue.state !== 'cached') { // Only needs to update if the response is cached return 'ignore'; } cachedValue.data = data; // This returned value will be returned in next calls to the cache. return cachedValue; } } } } ); ``` ### cache.etag - Type: `boolean` - Default: `true` If the request should handle [`ETag`](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Headers/ETag) and [`If-None-Match support`](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Headers/If-None-Match). Use a string to force a custom static value or true to use the previous response ETag. To use `true` (automatic ETag handling), `interpretHeader` option must be set to `true`. ### cache.modifiedSince - Type: `boolean` - Default: `true` Use [`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since) header in this request. Use a date to force a custom static value or true to use the last cached timestamp. If never cached before, the header is not set. If `interpretHeader` is set and a [`Last-Modified`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified) header is sent to us, then value from that header is used, otherwise cache creation timestamp will be sent in [`If-Modified-Since`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since). ### cache.staleIfError - Type: `number` or `boolean` or `StaleIfErrorPredicate` - Default: `true` Enables cache to be returned if the response comes with an error, either by invalid status code, network errors and etc. You can filter the type of error that should be stale by using a predicate function. ::: warning If the response is treated as error because of invalid status code _(like when using [statusCheck](#cache-cachepredicate))_, and this ends up `true`, the cache will be preserved over the "invalid" request. So, if you want to preserve the response, you can use the below predicate: ::: ```ts const customPredicate = (response, cache, error) => { // Blocks staleIfError if has a response return !response; // Note that, this still respects axios default implementation // and throws an error, (but it keeps the response) }; ``` Types: - `number` -> the max time (in seconds) that the cache can be reused. - `boolean` -> `false` disables and `true` enables with infinite time if no value is present on `stale-if-error` in Cache-Control. - `function` -> a predicate that can return `number` or `boolean` as described above. ### cache.override - Type: `boolean` - Default: `false` This option bypasses the current cache and always make a new http request. This will not delete the current cache, it will just replace the cache when the response arrives. Unlike as `cache: false`, this will not disable the cache, it will just ignore the pre-request cache checks before making the request. This way, all post-request options are still available and will work as expected. ### cache.hydrate - Type: `undefined | ((cache: StorageValue) => void | Promise)` - Default: `undefined` Asynchronously called when a network request is needed to resolve the data, but an older one **and probably expired** cache exists. Its with the current data **BEFORE** the network travel starts, so you can use it to temporarily update your UI with expired data before the network returns. Hydrating your components with old data before the network resolves with the newer one is better than _flickering_ your entire UI. This is even better when dealing with slower networks and persisted cache, like for mobile apps. ::: warning If the axios call will return cached data, meaning no network will be involved, the hydrate **IS NOT CALLED**, as the axios promise will be resolved instantly. ::: ```ts {7,13} // Example of function that receives data and renders into a screen function render() {} const response = await axios.get(/* [!code focus:10] */ 'url', { // This is called instantly if axios needs to make a network request cache: { hydrate: (cache) => render(cache.data) } }); // After the network lookup ends, we have fresh data and can // re-render the UI with confidence render(response.data); ``` --- ## Response object Axios cache interceptor returns a slightly changed than the original axios response. Containing information about the cache and other needed properties. ### id - Type: `string` The [Request ID](../guide/request-id.md) used in this request. It may have been generated by the [Key Generator](../guide/request-id.md#custom-generator) or a custom one provided by [`config.id`](./request-specifics.md#id) ```ts const response = await axios.get('url', { id: 'my-overridden-id' }); // This request used 'my-overridden-id' and // not the one generated by the key generator response.id === 'my-overridden-id'; ``` ### cached - Type: `boolean` A simple boolean indicating if the request returned data from the cache or from the network call. ::: tip This does not indicated if the request was capable of being cached or not, as options like [`cache.override`](./request-specifics.md#cache-override) may have been enabled. ::: ### stale - Type: `boolean` A simple boolean indicating if the request returned data is from valid or stale cache. --- ## Result Run at Sun, 29 Jan 2023 01:24:01 GMT Commit: 3db90e8c262c48d011fdd53bcda105512434e56e ### CACHE INTERCEPTOR - Operations: 41275/s - Network requests: 1 of 205788 - Performance: 100.00% ### CACHE ADAPTER - Operations: 37683/s - Network requests: 2 of 185780 - Performance: 91.30% ### AXIOS - Operations: 1970/s - Network requests: 9773 of 9773 - Performance: 4.77% --- ## setupCache() The `setupCache` function receives the axios instance and a set of optional properties described below. This modifies the axios instance in place and returns it. ```ts const axios = setupCache(axiosInstance, OPTIONS); ``` ::: tip The `setupCache` function receives global options and all [request specifics](./config/request-specifics.md) ones too. This way, you can customize the defaults for all requests. ::: ::: tip If you want to use the same cache interceptor for all your axios instances, you can call `setupCache` with the default axios instance. ```ts import Axios from 'axios'; setupCache(Axios, OPTIONS); ``` ::: ### location - Type: `InstanceLocation` - Default: `typeof window === 'undefined' ? 'server' : 'client'` A hint to the library about where the axios instance is being used. Used to take some decisions like handling or not `Cache-Control: private`. ```ts // NodeJS const cache = setupCache(Axios.create(), { location: 'server' }); // Browser const cache = setupCache(Axios.create(), { location: 'client' }); ``` ### storage - Type: `AxiosStorage` - Default: `buildMemoryStorage()` A storage interface is the entity responsible for saving, retrieving and serializing data received from network and requested when a axios call is made. See the [Storages](./guide/storages.md) page for more information. ### generateKey - Type: `KeyGenerator` - Default: `defaultKeyGenerator` The `generateKey` property defines the function responsible for generating unique keys for each request cache. By default, it employs a strategy that prioritizes the `id` if available, falling back to a string generated using various request properties. The default implementation generates a 32-bit hash key using the `method`, `baseURL`, `params`, `data`, and `url` of the request. ::: warning In any persistent cache scenario where hitting over 77K unique keys is a possibility, you should use a more robust hashing algorithm. [Read more](./guide/request-id.md#custom-generator) ::: ### waiting - Type: `Map>` - Default: `new Map` A simple object that will hold a promise for each pending request. Used to handle concurrent requests. You shouldn't change this property, but it is exposed in case you need to use it as some sort of listener or know when a request is waiting for others to finish. ### headerInterpreter - Type: `HeaderInterpreter` - Default: `defaultHeaderInterpreter` The function used to interpret all headers from a request and determine a time to live (`ttl`) number. ::: warning Many REST backends returns some variation of `Cache-Control: no-cache` or `Cache-Control: no-store` headers, which tell us to ignore caching at all. You shall disable `headerInterpreter` for those requests. _If the debug mode prints `Cache header interpreted as 'dont cache'` this is probably the reason._ ::: The possible returns are: - `'dont cache'`: the request will not be cached. - `'not enough headers'`: the request will find other ways to determine the TTL value. - `number`: used as the TTL value. - `{ cache: number, stale: number }`: used as the TTL value and stale TTL value ::: details Example of a custom headerInterpreter ```ts import { setupCache, type HeaderInterpreter } from 'axios-cache-interceptor'; const myHeaderInterpreter: HeaderInterpreter = (headers) => { if (headers['x-my-custom-header']) { const seconds = Number(headers['x-my-custom-header']); if (seconds < 1) { return 'dont cache'; } return seconds; } return 'not enough headers'; }; ``` ::: ### requestInterceptor - Type: `AxiosInterceptor>` - Default: `defaultRequestInterceptor()` The function that will be used to intercept the request before it is sent to the axios adapter. It is the main function of this library, as it is the bridge between the axios request and the cache. It wasn't meant to be changed, but if you need to, you can do it by passing a new function to this property. See its code for more information [here](https://github.com/arthurfiorette/axios-cache-interceptor/tree/main/src/interceptors). ### responseInterceptor - Type: `AxiosInterceptor>` - Default: `defaultResponseInterceptor()` The function that will be used to intercept the request after it is returned by the axios adapter. It is the second most important function of this library, as it is the bridge between the axios response and the cache. It wasn't meant to be changed, but if you need to, you can do it by passing a new function to this property. See its code for more information [here](https://github.com/arthurfiorette/axios-cache-interceptor/tree/main/src/interceptors). ### debug - Type: `(msg: { id?: string; msg?: string; data?: unknown }) => void` or `undefined` - Default: `undefined` ::: warning This option only works when targeting a [Development](./guide/debugging.md) build. ::: The debug option will print debug information in the console. It is good if you need to trace any undesired behavior or issue. You can enable it by setting `debug` to a function that receives an string and returns nothing. Read the [Debugging](./guide/debugging.md) page for the complete guide. ::: details Example of a custom debug function ```ts // Will print debug info in the console. setupCache(axiosInstance, { debug: console.log }); // Own logging platform. setupCache(axiosInstance, { debug: ({ id, msg, data }) => myLoggerExample.emit({ id, msg, data }) }); // Disables debug. (default) setupCache(axiosInstance, { debug: undefined }); ``` ::: --- ## Storages Storages are responsible for saving, retrieving and serializing (if needed) cache data. They are completely customizable and you can code your own, or use one published on NPM. They are meant to be the middleware between the cache interceptor and some sort of database (persistent or not) you may have. Our interceptors will call its methods internally to save and retrieve data, but you can do it manually to work programmatically your own way. Currently, two storages are included in the library by default: - [Memory Storage](#memory-storage) accessible with `buildMemoryStorage` _(works on Node and Web)_ - [Web Storage API](#web-storage-api) accessible with `buildWebStorage` _(works on Web only)_ ### Memory Storage ::: warning **This is the storage chosen by default** ::: Memory storage is the simplest one. It works everywhere and its values are lost upon page reload or when the process is killed. If you are directly mutating some response property, you probably will face some reference issues because the storage will also get mutated. To avoid that, you can use the `clone: true` option to clone the response before saving it or `clone: 'double'` to also clone both ways, on `set()` and on `get()`. _Just like [#136](https://github.com/arthurfiorette/axios-cache-interceptor/issues/163) and many others._ For long running processes, you can avoid memory leaks by using playing with the `cleanupInterval` option. And can reduce memory usage with `maxEntries`. ```ts import Axios from 'axios'; import { setupCache, buildMemoryStorage } from 'axios-cache-interceptor'; setupCache(axios, { // You don't need to to that, as it is the default option. storage: buildMemoryStorage( /* cloneData default=*/ false, /* cleanupInterval default=*/ false, /* maxEntries default=*/ false ) }); ``` Options: - **cloneData**: Use `true` if the data returned by `find()` should be cloned to avoid mutating the original data outside the `set()` method. Use `'double'` to also clone before saving value in storage using `set()`. Disabled is default - **cleanupInterval**: The interval in milliseconds to run a setInterval job of cleaning old entries. If false, the job will not be created. Disabled is default - **maxEntries**: The maximum number of entries to keep in the storage. Its hard to determine the size of the entries, so a smart FIFO order is used to determine eviction. If false, no check will be done and you may grow up memory usage. Disabled is default ### Web Storage API If you need persistent caching between page refreshes, you can use the `buildWebStorage` to get this behavior. It works by connecting our storage API to the browser's [Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Storage). ::: code-group ```ts{7} [Local Storage] import Axios from 'axios'; import { setupCache, buildWebStorage } from 'axios-cache-interceptor'; setupCache(axios, { // [!code focus:5] // As localStorage is a public storage, you can add a prefix // to all keys to avoid collisions with other code. storage: buildWebStorage(localStorage, 'axios-cache:') }); ``` ```ts{7} [Session Storage] import Axios from 'axios'; import { setupCache, buildWebStorage } from 'axios-cache-interceptor'; setupCache(axios, { // [!code focus:5] // As sessionStorage is a public storage, you can add a prefix // to all keys to avoid collisions with other code. storage: buildWebStorage(sessionStorage, 'axios-cache:') }); ``` ```ts{4,7} [Custom Storage] import Axios from 'axios'; import { setupCache, buildWebStorage } from 'axios-cache-interceptor'; const myStorage = new Storage(); // [!code focus:5] setupCache(axios, { storage: buildWebStorage(myStorage) }); ``` ::: #### Browser quota From `v0.9.0` onwards, web storage is able to detect and evict older entries if the browser's quota is reached. The eviction is done by the following algorithm: 1. Just saved an value and got an error. _(Probably quota exceeded)_ 2. Evicts all expired keys that cannot enter the `stale` state. 3. If it fails again, evicts the oldest key with the given prefix. 4. Repeat step 2 and 3 until the object can be saved or the storage has been emptied. 5. If it still fails, the data is not saved. _Probably because the whole key is greater than the quota or other libraries already consumed the whole usable space._ ### buildStorage() All integrated storages are wrappers around the `buildStorage` function. External libraries use it and if you want to build your own, `buildStorage` is the way to go! The exported `buildStorage` function abstracts the storage interface and requires a super simple object to build the storage. It has 3 methods: - `set(key: string, value: NotEmptyStorageValue, currentRequest?: CacheRequestConfig): MaybePromise`: Receives the key and the value, and optionally the current request. It should save the value in the storage. - `remove(key: string, currentRequest?: CacheRequestConfig): MaybePromise`: Receives the key and optionally the current request. It should remove the value from the storage. - `find(key: string, currentRequest?: CacheRequestConfig) => MaybePromise`: Receives the key and optionally the current request. It should return the value from the storage or `undefined` if not found. - `clear() => MaybePromise`: Clears all data from storage. **This method isn't used by the interceptor itself**, instead, its here for you to use it programmatically. ### Third Party Storages These are not guaranteed to work with the latest version of the library as neither are maintained by the axios cache interceptor team. But, as we provide a minimal interface for storages, you can use them as a base to also create your own. - [Node Redis v4](#node-redis-storage) - [IndexedDb](#indexeddb) - [Node Cache](#node-cache) - **Have another one?** - [Open a PR](https://github.com/arthurfiorette/axios-cache-interceptor/pulls) to add it here. ### Node Redis storage The node redis storage implementation is listed here because it shows the only tricky part when implementing a storage with an third party client that allows auto-evicting entries, as show on the `PXAT` property. ```ts{4} import { createClient } from 'redis'; // v4 import { buildStorage, canStale } from 'axios-cache-interceptor'; const client = createClient(/* connection config */); // [!code focus:36] const redisStorage = buildStorage({ find(key) { return client .get(`axios-cache-${key}`) .then((result) => result && (JSON.parse(result) as StorageValue)); }, set(key, value, req) { return client.set(`axios-cache-${key}`, JSON.stringify(value), { PXAT: // We don't want to keep indefinitely values in the storage if // their request don't finish somehow. Either set its value as // the TTL or 1 minute. value.state === 'loading' ? Date.now() + (req?.cache && typeof req.cache.ttl === 'number' ? req.cache.ttl : // 1 minute in seconds 60000) : // When a stale state has a determined value to expire, we can use it. // Or if the cached value cannot enter in stale state. (value.state === 'stale' && value.ttl) || (value.state === 'cached' && !canStale(value)) ? value.createdAt + value.ttl! : // otherwise, we can't determine when it should expire, so we keep // it indefinitely. undefined }); }, remove(key) { return client.del(`axios-cache-${key}`); } }); ``` However you can use the [`buildStorage`](#buildstorage) function to integrate with ANY storage you want, like `localForage`, `ioredis`, `memcache` and others. ### IndexedDB Here is an example of how to use the `idb-keyval` library to create a storage that uses IndexedDB. ```ts import axios from 'axios'; import { buildStorage } from 'axios-cache-interceptor'; import { clear, del, get, set } from 'idb-keyval'; const indexedDbStorage = buildStorage({ async find(key) { const value = await get(key); if (!value) { return; } return JSON.parse(value); }, async set(key, value) { await set(key, JSON.stringify(value)); }, async remove(key) { await del(key); } }); ``` #### Node Cache This example implementation uses [node-cache](https://github.com/node-cache/node-cache) as a storage method. Do note that this library is somewhat old, however it appears to work at the time of writing. ```ts import { buildStorage } from "axios-cache-interceptor"; import NodeCache from "node-cache"; const cache = new NodeCache({ stdTTL: 60 * 60 * 24 * 7 }); const cacheStorage = buildStorage({ find(key) { return cache.get(key) } set(key, value) { cache.set(key, value); }, remove(key) { cache.del(key); }, }); ```