feat: add staleIfError support

This commit is contained in:
arthurfiorette 2022-01-18 12:06:20 -03:00
parent 8273399746
commit edb32bdea3
No known key found for this signature in database
GPG Key ID: 9D190CD53C53C555
13 changed files with 526 additions and 70 deletions

View File

@ -11,7 +11,8 @@
"plugin:prettier/recommended" "plugin:prettier/recommended"
], ],
"rules": { "rules": {
"@typescript-eslint/await-thenable": "off" "@typescript-eslint/await-thenable": "off",
"@typescript-eslint/restrict-template-expressions": "off"
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",

4
src/cache/axios.ts vendored
View File

@ -77,8 +77,8 @@ export interface AxiosCacheInstance extends CacheInstance, AxiosInstance {
}; };
interceptors: { interceptors: {
request: AxiosInterceptorManager<CacheRequestConfig<unknown, unknown>>; request: AxiosInterceptorManager<CacheRequestConfig>;
response: AxiosInterceptorManager<CacheAxiosResponse<unknown, unknown>>; response: AxiosInterceptorManager<CacheAxiosResponse>;
}; };
/** @template D The type that the request body use */ /** @template D The type that the request body use */

42
src/cache/cache.ts vendored
View File

@ -3,7 +3,12 @@ import type { Deferred } from 'fast-defer';
import type { HeadersInterpreter } from '../header/types'; import type { HeadersInterpreter } from '../header/types';
import type { AxiosInterceptor } from '../interceptors/build'; import type { AxiosInterceptor } from '../interceptors/build';
import type { AxiosStorage, CachedResponse } from '../storage/types'; 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'; 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 * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
*/ */
modifiedSince: Date | boolean; 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 { export interface CacheInstance {
@ -107,8 +143,8 @@ export interface CacheInstance {
headerInterpreter: HeadersInterpreter; headerInterpreter: HeadersInterpreter;
/** The request interceptor that will be used to handle the cache. */ /** 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. */ /** The response interceptor that will be used to handle the cache. */
responseInterceptor: AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>; responseInterceptor: AxiosInterceptor<CacheAxiosResponse>;
} }

6
src/cache/create.ts vendored
View File

@ -67,9 +67,7 @@ export function setupCache(
options.responseInterceptor || defaultResponseInterceptor(axiosCache); options.responseInterceptor || defaultResponseInterceptor(axiosCache);
// CacheRequestConfig values // CacheRequestConfig values
axiosCache.defaults = { axiosCache.defaults.cache = {
...axios.defaults,
cache: {
ttl: options.ttl ?? 1000 * 60 * 5, ttl: options.ttl ?? 1000 * 60 * 5,
interpretHeader: options.interpretHeader ?? false, interpretHeader: options.interpretHeader ?? false,
methods: options.methods || ['get'], methods: options.methods || ['get'],
@ -78,8 +76,8 @@ export function setupCache(
}, },
etag: options.etag ?? false, etag: options.etag ?? false,
modifiedSince: options.modifiedSince ?? false, modifiedSince: options.modifiedSince ?? false,
staleIfError: options.staleIfError ?? false,
update: options.update || {} update: options.update || {}
}
}; };
// Apply interceptors // Apply interceptors

View File

@ -3,9 +3,12 @@ import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios';
/** See {@link AxiosInterceptorManager} */ /** See {@link AxiosInterceptorManager} */
export interface AxiosInterceptor<T> { export interface AxiosInterceptor<T> {
onFulfilled?(value: T): T | Promise<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; apply: () => void;
} }
export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig<unknown, unknown>>; export type RequestInterceptor = AxiosInterceptor<CacheRequestConfig>;
export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse<unknown, unknown>>; export type ResponseInterceptor = AxiosInterceptor<CacheAxiosResponse>;

View File

@ -10,7 +10,7 @@ import {
ConfigWithCache, ConfigWithCache,
createValidateStatus, createValidateStatus,
isMethodIn, isMethodIn,
setRevalidationHeaders updateStaleRequest
} from './util'; } from './util';
export function defaultRequestInterceptor(axios: AxiosCacheInstance) { export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
@ -56,11 +56,17 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
await axios.storage.set(key, { await axios.storage.set(key, {
state: 'loading', 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') { if (cache.state === 'stale') {
setRevalidationHeaders(cache, config as ConfigWithCache<unknown>); updateStaleRequest(cache, config as ConfigWithCache<unknown>);
} }
config.validateStatus = createValidateStatus(config.validateStatus); config.validateStatus = createValidateStatus(config.validateStatus);
@ -92,7 +98,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance) {
// Even though the response interceptor receives this one from here, // Even though the response interceptor receives this one from here,
// it has been configured to ignore cached responses = true // it has been configured to ignore cached responses = true
config.adapter = (): Promise<CacheAxiosResponse<unknown, unknown>> => config.adapter = (): Promise<CacheAxiosResponse> =>
Promise.resolve({ Promise.resolve({
config, config,
data: cachedResponse.data, data: cachedResponse.data,

View File

@ -1,5 +1,9 @@
import type { AxiosCacheInstance } from '../cache/axios'; import type { CacheProperties } from '..';
import type { CacheProperties } from '../cache/cache'; import type {
AxiosCacheInstance,
CacheAxiosResponse,
CacheRequestConfig
} from '../cache/axios';
import type { CachedStorageValue } from '../storage/types'; import type { CachedStorageValue } from '../storage/types';
import { testCachePredicate } from '../util/cache-predicate'; import { testCachePredicate } from '../util/cache-predicate';
import { Header } from '../util/headers'; import { Header } from '../util/headers';
@ -15,15 +19,12 @@ export function defaultResponseInterceptor(
* *
* Also update the waiting list for this key by rejecting it. * Also update the waiting list for this key by rejecting it.
*/ */
const rejectResponse = async ( const rejectResponse = async (responseId: string) => {
{ storage, waiting }: AxiosCacheInstance,
responseId: string
) => {
// Update the cache to empty to prevent infinite loading state // Update the cache to empty to prevent infinite loading state
await storage.remove(responseId); await axios.storage.remove(responseId);
// Reject the deferred if present // Reject the deferred if present
waiting[responseId]?.reject(null); axios.waiting[responseId]?.reject(null);
delete waiting[responseId]; delete axios.waiting[responseId];
}; };
const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => { const onFulfilled: ResponseInterceptor['onFulfilled'] = async (response) => {
@ -41,6 +42,7 @@ export function defaultResponseInterceptor(
return { ...response, cached: false }; return { ...response, cached: false };
} }
// Request interceptor merges defaults with per request configuration
const cacheConfig = response.config.cache as CacheProperties; const cacheConfig = response.config.cache as CacheProperties;
const cache = await axios.storage.get(response.id); const cache = await axios.storage.get(response.id);
@ -61,13 +63,18 @@ export function defaultResponseInterceptor(
!cache.data && !cache.data &&
!(await testCachePredicate(response, cacheConfig.cachePredicate)) !(await testCachePredicate(response, cacheConfig.cachePredicate))
) { ) {
await rejectResponse(axios, response.id); await rejectResponse(response.id);
return response; return response;
} }
// avoid remnant headers from remote server to break implementation // avoid remnant headers from remote server to break implementation
delete response.headers[Header.XAxiosCacheEtag]; for (const header in Header) {
delete response.headers[Header.XAxiosCacheLastModified]; if (!header.startsWith('XAxiosCache')) {
continue;
}
delete response.headers[header];
}
if (cacheConfig.etag && cacheConfig.etag !== true) { if (cacheConfig.etag && cacheConfig.etag !== true) {
response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag; response.headers[Header.XAxiosCacheEtag] = cacheConfig.etag;
@ -87,7 +94,7 @@ export function defaultResponseInterceptor(
// Cache should not be used // Cache should not be used
if (expirationTime === 'dont cache') { if (expirationTime === 'dont cache') {
await rejectResponse(axios, response.id); await rejectResponse(response.id);
return response; return response;
} }
@ -100,6 +107,15 @@ export function defaultResponseInterceptor(
ttl = await ttl(response); 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 = { const newCache: CachedStorageValue = {
state: 'cached', state: 'cached',
ttl, ttl,
@ -107,15 +123,8 @@ export function defaultResponseInterceptor(
data 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 // Resolve all other requests waiting for this response
deferred?.resolve(newCache.data); axios.waiting[response.id]?.resolve(newCache.data);
delete axios.waiting[response.id]; delete axios.waiting[response.id];
// Define this key as cache on the storage // Define this key as cache on the storage
@ -125,8 +134,74 @@ export function defaultResponseInterceptor(
return response; 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 { return {
onFulfilled, onFulfilled,
apply: () => axios.interceptors.response.use(onFulfilled) onRejected,
apply: () => axios.interceptors.response.use(onFulfilled, onRejected)
}; };
} }

View File

@ -36,7 +36,11 @@ export type ConfigWithCache<D> = CacheRequestConfig<unknown, D> & {
cache: Partial<CacheProperties>; 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, cache: StaleStorageValue,
config: ConfigWithCache<D> config: ConfigWithCache<D>
): void { ): void {

View File

@ -56,9 +56,11 @@ export function buildStorage({ set, find, remove }: BuildStorage): AxiosStorage
if ( if (
value.data.headers && value.data.headers &&
// Any header below allows the response to stale
(Header.ETag in value.data.headers || (Header.ETag in value.data.headers ||
Header.LastModified in value.data.headers || Header.LastModified in value.data.headers ||
Header.XAxiosCacheEtag in value.data.headers || Header.XAxiosCacheEtag in value.data.headers ||
Header.XAxiosCacheStaleIfError in value.data.headers ||
Header.XAxiosCacheLastModified in value.data.headers) Header.XAxiosCacheLastModified in value.data.headers)
) { ) {
const stale: StaleStorageValue = { const stale: StaleStorageValue = {

View File

@ -16,6 +16,12 @@ export type StorageValue =
export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>; export type NotEmptyStorageValue = Exclude<StorageValue, EmptyStorageValue>;
export type StorageMetadata = {
/** If the request can be stale */
shouldStale?: boolean;
[key: string]: unknown;
};
export type StaleStorageValue = { export type StaleStorageValue = {
data: CachedResponse; data: CachedResponse;
ttl?: undefined; ttl?: undefined;
@ -31,18 +37,21 @@ export type CachedStorageValue = {
state: 'cached'; state: 'cached';
}; };
export type LoadingStorageValue = { export type LoadingStorageValue =
/** | {
* Only present if the previous state was `stale`. So, in case the new response comes data?: undefined;
* without a value, this data is used ttl?: undefined;
*/
data?: CachedResponse;
ttl?: number;
/** Defined when the state is cached */
createdAt?: undefined; createdAt?: undefined;
state: 'loading'; state: 'loading';
}; previous: 'empty';
}
| {
state: 'loading';
data: CachedResponse;
ttl?: undefined;
createdAt: number;
previous: 'stale';
};
export type EmptyStorageValue = { export type EmptyStorageValue = {
data?: undefined; data?: undefined;

View File

@ -28,7 +28,13 @@ export const Header = Object.freeze({
*/ */
IfNoneMatch: 'if-none-match', 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', CacheControl: 'cache-control',
/** /**
@ -70,8 +76,8 @@ export const Header = Object.freeze({
ContentType: 'content-type', ContentType: 'content-type',
/** /**
* Used internally to mark the cache item as being revalidatable and enabling stale * Used internally as metadata to mark the cache item as revalidatable and enabling
* cache state Contains a string of ASCII characters that can be used as ETag for * 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. * `If-Match` header Provided by user using `cache.etag` value.
* *
* ```txt * ```txt
@ -81,16 +87,27 @@ export const Header = Object.freeze({
XAxiosCacheEtag: 'x-axios-cache-etag', XAxiosCacheEtag: 'x-axios-cache-etag',
/** /**
* Used internally to mark the cache item as being revalidatable and enabling stale * Used internally as metadata to mark the cache item as revalidatable and enabling
* cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is `true`, * stale cache state may contain `'use-cache-timestamp'` if `cache.modifiedSince` is
* otherwise will contain a date from `cache.modifiedSince`. If a date is provided, it * `true`, otherwise will contain a date from `cache.modifiedSince`. If a date is
* can be used for `If-Modified-Since` header, otherwise the cache timestamp can be used * provided, it can be used for `If-Modified-Since` header, otherwise the cache
* for `If-Modified-Since` header. * timestamp can be used for `If-Modified-Since` header.
* *
* ```txt * ```txt
* X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT * X-Axios-Cache-Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
* X-Axios-Cache-Last-Modified: use-cache-timestamp * 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'
}); });

View File

@ -41,3 +41,20 @@ export type CacheUpdater<R, D> =
cached: Exclude<StorageValue, LoadingStorageValue>, cached: Exclude<StorageValue, LoadingStorageValue>,
response: CacheAxiosResponse<R, D> response: CacheAxiosResponse<R, D>
) => MaybePromise<CachedStorageValue | 'delete' | 'ignore'>); ) => 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>);

View 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);
});
});