mirror of
https://github.com/arthurfiorette/axios-cache-interceptor.git
synced 2025-12-08 17:36:16 +00:00
feat: add staleIfError support
This commit is contained in:
parent
8273399746
commit
edb32bdea3
@ -11,7 +11,8 @@
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/await-thenable": "off"
|
||||
"@typescript-eslint/await-thenable": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
|
||||
4
src/cache/axios.ts
vendored
4
src/cache/axios.ts
vendored
@ -77,8 +77,8 @@ export interface AxiosCacheInstance extends CacheInstance, AxiosInstance {
|
||||
};
|
||||
|
||||
interceptors: {
|
||||
request: AxiosInterceptorManager<CacheRequestConfig<unknown, unknown>>;
|
||||
response: AxiosInterceptorManager<CacheAxiosResponse<unknown, unknown>>;
|
||||
request: AxiosInterceptorManager<CacheRequestConfig>;
|
||||
response: AxiosInterceptorManager<CacheAxiosResponse>;
|
||||
};
|
||||
|
||||
/** @template D The type that the request body use */
|
||||
|
||||
42
src/cache/cache.ts
vendored
42
src/cache/cache.ts
vendored
@ -3,7 +3,12 @@ import type { Deferred } from 'fast-defer';
|
||||
import type { HeadersInterpreter } from '../header/types';
|
||||
import type { AxiosInterceptor } from '../interceptors/build';
|
||||
import type { AxiosStorage, CachedResponse } from '../storage/types';
|
||||
import type { CachePredicate, CacheUpdater, KeyGenerator } from '../util/types';
|
||||
import type {
|
||||
CachePredicate,
|
||||
CacheUpdater,
|
||||
KeyGenerator,
|
||||
StaleIfErrorPredicate
|
||||
} from '../util/types';
|
||||
import type { CacheAxiosResponse, CacheRequestConfig } from './axios';
|
||||
|
||||
/**
|
||||
@ -76,6 +81,37 @@ export type CacheProperties<R = unknown, D = unknown> = {
|
||||
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
|
||||
*/
|
||||
modifiedSince: Date | boolean;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* **Note**: If this value ends up `false`, either by default or by a predicate function
|
||||
* and there was an error, the request cache will be purged.
|
||||
*
|
||||
* **Note**: If the response is treated as error because of invalid status code *(like
|
||||
* from AxiosRequestConfig#invalidateStatus)*, 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 this predicate:
|
||||
*
|
||||
* ```js
|
||||
* const customPredicate = (response, cache, error) => {
|
||||
* // Return false if has a response
|
||||
* return !response;
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* Possible types:
|
||||
*
|
||||
* - `number` -> the max time (in seconds) that the cache can be reused.
|
||||
* - `boolean` -> `false` disables and `true` enables with infinite time.
|
||||
* - `function` -> a predicate that can return `number` or `boolean` as described above.
|
||||
*
|
||||
* @default false
|
||||
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error
|
||||
*/
|
||||
staleIfError: StaleIfErrorPredicate<R, D>;
|
||||
};
|
||||
|
||||
export interface CacheInstance {
|
||||
@ -107,8 +143,8 @@ export interface CacheInstance {
|
||||
headerInterpreter: HeadersInterpreter;
|
||||
|
||||
/** The request interceptor that will be used to handle the cache. */
|
||||
requestInterceptor: AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
||||
requestInterceptor: AxiosInterceptor<CacheRequestConfig>;
|
||||
|
||||
/** The response interceptor that will be used to handle the cache. */
|
||||
responseInterceptor: AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
||||
responseInterceptor: AxiosInterceptor<CacheAxiosResponse>;
|
||||
}
|
||||
|
||||
24
src/cache/create.ts
vendored
24
src/cache/create.ts
vendored
@ -67,19 +67,17 @@ export function setupCache(
|
||||
options.responseInterceptor || defaultResponseInterceptor(axiosCache);
|
||||
|
||||
// CacheRequestConfig values
|
||||
axiosCache.defaults = {
|
||||
...axios.defaults,
|
||||
cache: {
|
||||
ttl: options.ttl ?? 1000 * 60 * 5,
|
||||
interpretHeader: options.interpretHeader ?? false,
|
||||
methods: options.methods || ['get'],
|
||||
cachePredicate: options.cachePredicate || {
|
||||
statusCheck: (status) => status >= 200 && status < 400
|
||||
},
|
||||
etag: options.etag ?? false,
|
||||
modifiedSince: options.modifiedSince ?? false,
|
||||
update: options.update || {}
|
||||
}
|
||||
axiosCache.defaults.cache = {
|
||||
ttl: options.ttl ?? 1000 * 60 * 5,
|
||||
interpretHeader: options.interpretHeader ?? false,
|
||||
methods: options.methods || ['get'],
|
||||
cachePredicate: options.cachePredicate || {
|
||||
statusCheck: (status) => status >= 200 && status < 400
|
||||
},
|
||||
etag: options.etag ?? false,
|
||||
modifiedSince: options.modifiedSince ?? false,
|
||||
staleIfError: options.staleIfError ?? false,
|
||||
update: options.update || {}
|
||||
};
|
||||
|
||||
// Apply interceptors
|
||||
|
||||
@ -3,9 +3,12 @@ import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
|
||||
/** See {@link AxiosInterceptorManager} */
|
||||
export interface AxiosInterceptor<T> {
|
||||
onFulfilled?(value: T): T | Promise<T>;
|
||||
onRejected?(error: unknown): unknown;
|
||||
|
||||
/** Returns a successful response or re-throws the error */
|
||||
onRejected?(error: Record<string, unknown>): T | Promise<T>;
|
||||
|
||||
apply: () => void;
|
||||
}
|
||||
|
||||
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>;
|
||||
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>;
|
||||
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig>;
|
||||
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse>;
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
ConfigWithCache,
|
||||
createValidateStatus,
|
||||
isMethodIn,
|
||||
setRevalidationHeaders
|
||||
updateStaleRequest
|
||||
} from './util';
|
||||
|
||||
export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
@ -56,11 +56,17 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
|
||||
await axios.storage.set(key, {
|
||||
state: 'loading',
|
||||
data: cache.data
|
||||
previous: cache.state,
|
||||
|
||||
// Eslint complains a lot :)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
data: cache.data as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
||||
createdAt: cache.createdAt as any
|
||||
});
|
||||
|
||||
if (cache.state === 'stale') {
|
||||
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>);
|
||||
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
|
||||
}
|
||||
|
||||
config.validateStatus = createValidateStatus(config.validateStatus);
|
||||
@ -92,7 +98,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
|
||||
|
||||
// Even though the response interceptor receives this one from here,
|
||||
// it has been configured to ignore cached responses = true
|
||||
config.adapter = (): Promise<CacheAxiosResponse<unknown, unknown>> =>
|
||||
config.adapter = (): Promise<CacheAxiosResponse> =>
|
||||
Promise.resolve({
|
||||
config,
|
||||
data: cachedResponse.data,
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import type { AxiosCacheInstance } from '../cache/axios';
|
||||
import type { CacheProperties } from '../cache/cache';
|
||||
import type { CacheProperties } from '..';
|
||||
import type {
|
||||
AxiosCacheInstance,
|
||||
CacheAxiosResponse,
|
||||
CacheRequestConfig
|
||||
} from '../cache/axios';
|
||||
import type { CachedStorageValue } from '../storage/types';
|
||||
import { testCachePredicate } from '../util/cache-predicate';
|
||||
import { Header } from '../util/headers';
|
||||
@ -15,15 +19,12 @@ export function defaultResponseInterceptor(
|
||||
*
|
||||
* Also update the waiting list for this key by rejecting it.
|
||||
*/
|
||||
const rejectResponse = async (
|
||||
{ storage, waiting }: AxiosCacheInstance,
|
||||
responseId: string
|
||||
) => {
|
||||
const rejectResponse = async (responseId: string) => {
|
||||
// Update the cache to empty to prevent infinite loading state
|
||||
await storage.remove(responseId);
|
||||
await axios.storage.remove(responseId);
|
||||
// Reject the deferred if present
|
||||
waiting[responseId]?.reject(null);
|
||||
delete waiting[responseId];
|
||||
axios.waiting[responseId]?.reject(null);
|
||||
delete axios.waiting[responseId];
|
||||
};
|
||||
|
||||
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
|
||||
@ -41,6 +42,7 @@ export function defaultResponseInterceptor(
|
||||
return { ...response, cached: false };
|
||||
}
|
||||
|
||||
// Request interceptor merges defaults with per request configuration
|
||||
const cacheConfig = response.config.cache as CacheProperties;
|
||||
|
||||
const cache = await axios.storage.get(response.id);
|
||||
@ -61,13 +63,18 @@ export function defaultResponseInterceptor(
|
||||
!cache.data &&
|
||||
!(await testCachePredicate(response, cacheConfig.cachePredicate))
|
||||
) {
|
||||
await rejectResponse(axios, response.id);
|
||||
await rejectResponse(response.id);
|
||||
return response;
|
||||
}
|
||||
|
||||
// avoid remnant headers from remote server to break implementation
|
||||
delete response.headers[Header.XAxiosCacheEtag];
|
||||
delete response.headers[Header.XAxiosCacheLastModified];
|
||||
for (const header in Header) {
|
||||
if (!header.startsWith('XAxiosCache')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete response.headers[header];
|
||||
}
|
||||
|
||||
if (cacheConfig.etag && cacheConfig.etag !== true) {
|
||||
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
|
||||
@ -87,7 +94,7 @@ export function defaultResponseInterceptor(
|
||||
|
||||
// Cache should not be used
|
||||
if (expirationTime === 'dont cache') {
|
||||
await rejectResponse(axios, response.id);
|
||||
await rejectResponse(response.id);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -100,6 +107,15 @@ export function defaultResponseInterceptor(
|
||||
ttl = await ttl(response);
|
||||
}
|
||||
|
||||
if (cacheConfig.staleIfError) {
|
||||
response.headers[Header.XAxiosCacheStaleIfError] = String(ttl);
|
||||
}
|
||||
|
||||
// Update other entries before updating himself
|
||||
if (cacheConfig?.update) {
|
||||
await updateCache(axios.storage, response, cacheConfig.update);
|
||||
}
|
||||
|
||||
const newCache: CachedStorageValue = {
|
||||
state: 'cached',
|
||||
ttl,
|
||||
@ -107,15 +123,8 @@ export function defaultResponseInterceptor(
|
||||
data
|
||||
};
|
||||
|
||||
// Update other entries before updating himself
|
||||
if (cacheConfig?.update) {
|
||||
await updateCache(axios.storage, response, cacheConfig.update);
|
||||
}
|
||||
|
||||
const deferred = axios.waiting[response.id];
|
||||
|
||||
// Resolve all other requests waiting for this response
|
||||
deferred?.resolve(newCache.data);
|
||||
axios.waiting[response.id]?.resolve(newCache.data);
|
||||
delete axios.waiting[response.id];
|
||||
|
||||
// Define this key as cache on the storage
|
||||
@ -125,8 +134,74 @@ export function defaultResponseInterceptor(
|
||||
return response;
|
||||
};
|
||||
|
||||
const onRejected: ResponseInterceptor['onRejected'] = async (error) => {
|
||||
const config = error['config'] as CacheRequestConfig;
|
||||
|
||||
if (!config || config.cache === false || !config.id) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const cache = await axios.storage.get(config.id);
|
||||
const cacheConfig = config.cache;
|
||||
|
||||
if (
|
||||
// This will only not be loading if the interceptor broke
|
||||
cache.state !== 'loading' ||
|
||||
cache.previous !== 'stale'
|
||||
) {
|
||||
await rejectResponse(config.id);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (cacheConfig?.staleIfError) {
|
||||
const staleIfError =
|
||||
typeof cacheConfig.staleIfError === 'function'
|
||||
? await cacheConfig.staleIfError(
|
||||
error.response as CacheAxiosResponse,
|
||||
cache,
|
||||
error
|
||||
)
|
||||
: cacheConfig.staleIfError;
|
||||
|
||||
if (
|
||||
staleIfError === true ||
|
||||
// staleIfError is the number of seconds that stale is allowed to be used
|
||||
(typeof staleIfError === 'number' && cache.createdAt + staleIfError > Date.now())
|
||||
) {
|
||||
const newCache: CachedStorageValue = {
|
||||
state: 'cached',
|
||||
ttl: Number(cache.data.headers[Header.XAxiosCacheStaleIfError]),
|
||||
createdAt: Date.now(),
|
||||
data: cache.data
|
||||
};
|
||||
|
||||
const response: CacheAxiosResponse = {
|
||||
cached: true,
|
||||
config,
|
||||
id: config.id,
|
||||
data: cache.data?.data,
|
||||
headers: cache.data?.headers,
|
||||
status: cache.data.status,
|
||||
statusText: cache.data.statusText
|
||||
};
|
||||
|
||||
// Resolve all other requests waiting for this response
|
||||
axios.waiting[response.id]?.resolve(newCache.data);
|
||||
delete axios.waiting[response.id];
|
||||
|
||||
// Valid response
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// Reject this response and rethrows the error
|
||||
await rejectResponse(config.id);
|
||||
throw error;
|
||||
};
|
||||
|
||||
return {
|
||||
onFulfilled,
|
||||
apply: () => axios.interceptors.response.use(onFulfilled)
|
||||
onRejected,
|
||||
apply: () => axios.interceptors.response.use(onFulfilled, onRejected)
|
||||
};
|
||||
}
|
||||
|
||||
@ -36,7 +36,11 @@ export type ConfigWithCache<D> = CacheRequestConfig<unknown, D> & {
|
||||
cache: Partial<CacheProperties>;
|
||||
};
|
||||
|
||||
export function setRevalidationHeaders<D>(
|
||||
/**
|
||||
* This function updates the cache when the request is stale. So, the next request to the
|
||||
* server will be made with proper header / settings.
|
||||
*/
|
||||
export function updateStaleRequest<D>(
|
||||
cache: StaleStorageValue,
|
||||
config: ConfigWithCache<D>
|
||||
): void {
|
||||
|
||||
@ -56,9 +56,11 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
|
||||
|
||||
if (
|
||||
value.data.headers &&
|
||||
// Any header below allows the response to stale
|
||||
(Header.ETag in value.data.headers ||
|
||||
Header.LastModified in value.data.headers ||
|
||||
Header.XAxiosCacheEtag in value.data.headers ||
|
||||
Header.XAxiosCacheStaleIfError in value.data.headers ||
|
||||
Header.XAxiosCacheLastModified in value.data.headers)
|
||||
) {
|
||||
const stale: StaleStorageValue = {
|
||||
|
||||
@ -16,6 +16,12 @@ export type StorageValue =
|
||||
|
||||
export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
|
||||
|
||||
export type StorageMetadata = {
|
||||
/** If the request can be stale */
|
||||
shouldStale?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type StaleStorageValue = {
|
||||
data: CachedResponse;
|
||||
ttl?: undefined;
|
||||
@ -31,18 +37,21 @@ export type CachedStorageValue = {
|
||||
state: 'cached';
|
||||
};
|
||||
|
||||
export type LoadingStorageValue = {
|
||||
/**
|
||||
* Only present if the previous state was `stale`. So, in case the new response comes
|
||||
* without a value, this data is used
|
||||
*/
|
||||
data?: CachedResponse;
|
||||
ttl?: number;
|
||||
|
||||
/** Defined when the state is cached */
|
||||
createdAt?: undefined;
|
||||
state: 'loading';
|
||||
};
|
||||
export type LoadingStorageValue =
|
||||
| {
|
||||
data?: undefined;
|
||||
ttl?: undefined;
|
||||
createdAt?: undefined;
|
||||
state: 'loading';
|
||||
previous: 'empty';
|
||||
}
|
||||
| {
|
||||
state: 'loading';
|
||||
data: CachedResponse;
|
||||
ttl?: undefined;
|
||||
createdAt: number;
|
||||
previous: 'stale';
|
||||
};
|
||||
|
||||
export type EmptyStorageValue = {
|
||||
data?: undefined;
|
||||
|
||||
@ -28,7 +28,13 @@ export const Header = Object.freeze({
|
||||
*/
|
||||
IfNoneMatch: 'if-none-match',
|
||||
|
||||
/** @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control */
|
||||
/**
|
||||
* ```txt
|
||||
* Cache-Control: max-age=604800
|
||||
* ```
|
||||
*
|
||||
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
*/
|
||||
CacheControl: 'cache-control',
|
||||
|
||||
/**
|
||||
@ -70,8 +76,8 @@ export const Header = Object.freeze({
|
||||
ContentType: 'content-type',
|
||||
|
||||
/**
|
||||
* Used internally to mark the cache item as being revalidatable and enabling stale
|
||||
* cache state Contains a string of ASCII characters that can be used as ETag for
|
||||
* Used internally as metadata to mark the cache item as revalidatable and enabling
|
||||
* stale cache state Contains a string of ASCII characters that can be used as ETag for
|
||||
* `If-Match` header Provided by user using `cache.etag` value.
|
||||
*
|
||||
* ```txt
|
||||
@ -81,16 +87,27 @@ export const Header = Object.freeze({
|
||||
XAxiosCacheEtag: 'x-axios-cache-etag',
|
||||
|
||||
/**
|
||||
* Used internally to mark the cache item as being revalidatable and enabling stale
|
||||
* cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is `true`,
|
||||
* otherwise will contain a date from `cache.modifiedSince`. If a date is provided, it
|
||||
* can be used for `If-Modified-Since` header, otherwise the cache timestamp can be used
|
||||
* for `If-Modified-Since` header.
|
||||
* Used internally as metadata to mark the cache item as revalidatable and enabling
|
||||
* stale cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is
|
||||
* `true`, otherwise will contain a date from `cache.modifiedSince`. If a date is
|
||||
* provided, it can be used for `If-Modified-Since` header, otherwise the cache
|
||||
* timestamp can be used for `If-Modified-Since` header.
|
||||
*
|
||||
* ```txt
|
||||
* X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
|
||||
* X-Axios-Cache-Last-Modified: use-cache-timestamp
|
||||
* ```
|
||||
*/
|
||||
XAxiosCacheLastModified: 'x-axios-cache-last-modified'
|
||||
XAxiosCacheLastModified: 'x-axios-cache-last-modified',
|
||||
|
||||
/**
|
||||
* Used internally as metadata to mark the cache item able to be used if the server
|
||||
* returns an error. The stale-if-error response directive indicates that the cache can
|
||||
* reuse a stale response when any error occurs.
|
||||
*
|
||||
* ```txt
|
||||
* XAxiosCacheStaleIfError: <seconds>
|
||||
* ```
|
||||
*/
|
||||
XAxiosCacheStaleIfError: 'x-axios-cache-stale-if-error'
|
||||
});
|
||||
|
||||
@ -41,3 +41,20 @@ export type CacheUpdater<R, D> =
|
||||
cached: Exclude<StorageValue, LoadingStorageValue>,
|
||||
response: CacheAxiosResponse<R, D>
|
||||
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>);
|
||||
|
||||
/**
|
||||
* You can use a `number` to ensure an max time (in seconds) that the cache can be reused.
|
||||
*
|
||||
* You can use `true` to use the cache until a new response is received.
|
||||
*
|
||||
* You can use a `function` predicate to determine if the cache can be reused (`boolean`)
|
||||
* or how much time the cache can be used (`number`)
|
||||
*/
|
||||
export type StaleIfErrorPredicate<R, D> =
|
||||
| number
|
||||
| boolean
|
||||
| ((
|
||||
networkResponse: CacheAxiosResponse<R, D> | undefined,
|
||||
cache: LoadingStorageValue & { previous: 'stale' },
|
||||
error: Record<string, unknown>
|
||||
) => MaybePromise<number | boolean>);
|
||||
|
||||
288
test/interceptors/stale-if-error.test.ts
Normal file
288
test/interceptors/stale-if-error.test.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import Axios from 'axios';
|
||||
import { setupCache } from '../../src/cache/create';
|
||||
import { Header } from '../../src/util/headers';
|
||||
import { mockAxios } from '../mocks/axios';
|
||||
|
||||
describe('Last-Modified handling', () => {
|
||||
it('expects that error is thrown', async () => {
|
||||
const instance = Axios.create({});
|
||||
const axios = setupCache(instance, {});
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown.url.lan:1234');
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
axios.defaults.cache.staleIfError = 10e5;
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown.url.lan:1234');
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
axios.defaults.cache.staleIfError = true;
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown.url.lan:1234');
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
expect.assertions(3);
|
||||
});
|
||||
|
||||
it('expects staleIfError does nothing without cache', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: () => Promise.resolve(true)
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown.url.lan:1234');
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
it('expects that XAxiosCacheStaleIfError is defined', async () => {
|
||||
const axios = mockAxios();
|
||||
|
||||
const { headers } = await axios.get('url', {
|
||||
cache: { staleIfError: true }
|
||||
});
|
||||
|
||||
expect(headers).toHaveProperty(Header.XAxiosCacheStaleIfError);
|
||||
});
|
||||
|
||||
it('expects staleIfError is ignore if config.cache is false', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: true
|
||||
});
|
||||
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
const id = 'some-config-id';
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown-url.lan:9090', {
|
||||
id,
|
||||
cache: false
|
||||
});
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
it('tests staleIfError', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: true
|
||||
});
|
||||
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
const id = 'some-config-id';
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
const response = await axios.get('http://unknown-url.lan:9090', {
|
||||
id,
|
||||
cache: { staleIfError: true }
|
||||
});
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(response.id).toBe(id);
|
||||
expect(response.data).toBe(cache.data);
|
||||
expect(response.status).toBe(cache.status);
|
||||
expect(response.statusText).toBe(cache.statusText);
|
||||
expect(response.headers).toBe(cache.headers);
|
||||
expect(response.cached).toBe(true);
|
||||
});
|
||||
|
||||
it('expects that staleIfError needs to be true', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: true
|
||||
});
|
||||
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
const id = 'some-config-id';
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown-url.lan:9090', {
|
||||
id,
|
||||
cache: { staleIfError: false }
|
||||
});
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
it('tests staleIfError returning false', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: () => false
|
||||
});
|
||||
|
||||
const id = 'some-config-id';
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown-url.lan:9090', {
|
||||
id
|
||||
});
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
expect.assertions(1);
|
||||
});
|
||||
|
||||
it('tests staleIfError as function', async () => {
|
||||
const axios = setupCache(Axios.create(), {
|
||||
staleIfError: () => {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
const id = 'some-config-id';
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown-url.lan:9090', { id });
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.get('http://unknown-url.lan:9090', {
|
||||
id,
|
||||
cache: {
|
||||
staleIfError: () => 1 // past
|
||||
}
|
||||
});
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(Axios.isAxiosError(error)).toBe(true);
|
||||
}
|
||||
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
const response = await axios.get('http://unknown-url.lan:9090', {
|
||||
id,
|
||||
cache: {
|
||||
staleIfError: () => 10e5 // nearly infinity :)
|
||||
}
|
||||
});
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(response.id).toBe(id);
|
||||
expect(response.data).toBe(cache.data);
|
||||
expect(response.status).toBe(cache.status);
|
||||
expect(response.statusText).toBe(cache.statusText);
|
||||
expect(response.headers).toBe(cache.headers);
|
||||
expect(response.cached).toBe(true);
|
||||
});
|
||||
|
||||
it('tests staleIfError with real 50X status code', async () => {
|
||||
const axios = setupCache(Axios.create(), { staleIfError: true });
|
||||
|
||||
const id = 'some-config-id';
|
||||
|
||||
const cache = {
|
||||
data: true,
|
||||
headers: {},
|
||||
status: 200,
|
||||
statusText: 'Ok'
|
||||
};
|
||||
|
||||
// Fill the cache
|
||||
await axios.storage.set(id, {
|
||||
state: 'stale',
|
||||
createdAt: Date.now(),
|
||||
data: cache
|
||||
});
|
||||
|
||||
const response = await axios.get('https://httpbin.org/status/503', {
|
||||
id
|
||||
});
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(response.id).toBe(id);
|
||||
expect(response.data).toBe(cache.data);
|
||||
expect(response.status).toBe(cache.status);
|
||||
expect(response.statusText).toBe(cache.statusText);
|
||||
expect(response.headers).toBe(cache.headers);
|
||||
expect(response.cached).toBe(true);
|
||||
|
||||
const newResponse = await axios.get('https://httpbin.org/status/503', {
|
||||
id,
|
||||
validateStatus: () => true // prevents error
|
||||
});
|
||||
|
||||
expect(newResponse).toBeDefined();
|
||||
expect(newResponse.id).toBe(id);
|
||||
expect(newResponse.data).not.toBe(cache.data);
|
||||
expect(newResponse.status).toBe(503);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user