copilot-swe-agent[bot] 23cdaa2350 Fix: Prevent If-None-Match and If-Modified-Since headers when override is true
Co-authored-by: arthurfiorette <47537704+arthurfiorette@users.noreply.github.com>
2025-12-08 13:24:10 +00:00

322 lines
10 KiB
TypeScript

import { deferred } from 'fast-defer';
import type { AxiosCacheInstance, CacheAxiosResponse } from '../cache/axios.js';
import { Header } from '../header/headers.js';
import type { CachedResponse, CachedStorageValue, LoadingStorageValue } from '../storage/types.js';
import { regexOrStringMatch } from '../util/cache-predicate.js';
import type { RequestInterceptor } from './build.js';
import {
type ConfigWithCache,
createValidateStatus,
isMethodIn,
updateStaleRequest
} from './util.js';
export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInterceptor {
const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {
config.id = axios.generateKey(config);
if (config.cache === false) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Ignoring cache because config.cache === false',
data: config
});
}
return config;
}
// merge defaults with per request configuration
config.cache = { ...axios.defaults.cache, ...config.cache };
// ignoreUrls (blacklist)
if (
typeof config.cache.cachePredicate === 'object' &&
config.cache.cachePredicate.ignoreUrls &&
config.url
) {
for (const url of config.cache.cachePredicate.ignoreUrls) {
if (regexOrStringMatch(url, config.url)) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: `Ignored because url (${config.url}) matches ignoreUrls (${config.cache.cachePredicate.ignoreUrls})`,
data: {
url: config.url,
cachePredicate: config.cache.cachePredicate
}
});
}
return config;
}
}
}
// allowUrls
if (
typeof config.cache.cachePredicate === 'object' &&
config.cache.cachePredicate.allowUrls &&
config.url
) {
let matched = false;
for (const url of config.cache.cachePredicate.allowUrls) {
if (regexOrStringMatch(url, config.url)) {
matched = true;
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: `Cached because url (${config.url}) matches allowUrls (${config.cache.cachePredicate.allowUrls})`,
data: {
url: config.url,
cachePredicate: config.cache.cachePredicate
}
});
}
break;
}
}
if (!matched) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: `Ignored because url (${config.url}) does not match any allowUrls (${config.cache.cachePredicate.allowUrls})`,
data: {
url: config.url,
cachePredicate: config.cache.cachePredicate
}
});
}
return config;
}
}
// Applies sufficient headers to prevent other cache systems to work along with this one
//
// Its currently used before isMethodIn because if the isMethodIn returns false, the request
// shouldn't be cached an therefore neither in the browser.
// https://stackoverflow.com/a/2068407
if (config.cache.cacheTakeover) {
config.headers[Header.CacheControl] ??= 'no-cache, no-store, must-revalidate';
config.headers[Header.Pragma] ??= 'no-cache';
config.headers[Header.Expires] ??= '0';
}
if (!isMethodIn(config.method, config.cache.methods)) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: `Ignored because method (${config.method}) is not in cache.methods (${config.cache.methods})`
});
}
return config;
}
// Assumes that the storage handled staled responses
let cache = await axios.storage.get(config.id, config);
const overrideCache = config.cache.override;
// Not cached, continue the request, and mark it as fetching
// biome-ignore lint/suspicious/noConfusingLabels: required to break condition in simultaneous accesses
ignoreAndRequest: if (
cache.state === 'empty' ||
cache.state === 'stale' ||
cache.state === 'must-revalidate' ||
overrideCache
) {
// This checks for simultaneous access to a new key. The js event loop jumps on the
// first await statement, so the second (asynchronous call) request may have already
// started executing.
if (axios.waiting.has(config.id) && !overrideCache) {
cache = (await axios.storage.get(config.id, config)) as
| CachedStorageValue
| LoadingStorageValue;
// @ts-expect-error This check is required when a request has it own cache deleted manually, lets
// say by a `axios.storage.delete(key)` and has a concurrent loading request.
// Because in this case, the cache will be empty and may still has a pending key
// on waiting map.
if (cache.state !== 'empty' && cache.state !== 'must-revalidate') {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Waiting list had an deferred for this key, waiting for it to finish'
});
}
break ignoreAndRequest;
}
}
// Create a deferred to resolve other requests for the same key when it's completed
const def = deferred<void>();
axios.waiting.set(config.id, def);
// Adds a default reject handler to catch when the request gets aborted without
// others waiting for it.
def.catch(() => undefined);
await axios.storage.set(
config.id,
{
state: 'loading',
previous: overrideCache
? // Simply determine if the request is stale or not
// based if it had previous data or not
cache.data
? 'stale'
: 'empty'
: // Typescript doesn't know that cache.state here can only be 'empty' or 'stale'
(cache.state as 'stale' | 'must-revalidate'),
data: cache.data as any,
// If the cache is empty and asked to override it, use the current timestamp
createdAt: overrideCache && !cache.createdAt ? Date.now() : (cache.createdAt as any)
},
config
);
if ((cache.state === 'stale' || cache.state === 'must-revalidate') && !overrideCache) {
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Updated stale request'
});
}
}
config.validateStatus = createValidateStatus(config.validateStatus);
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Sending request, waiting for response',
data: {
overrideCache,
state: cache.state
}
});
}
// Hydrates any UI temporarily, if cache is available
if (cache.state === 'stale' || (cache.data && cache.state !== 'must-revalidate')) {
await config.cache.hydrate?.(cache);
}
return config;
}
let cachedResponse: CachedResponse;
if (cache.state === 'loading') {
const deferred = axios.waiting.get(config.id);
// The deferred may not exists when the process is using a persistent
// storage and cancelled in the middle of a request, this would result in
// a pending loading state in the storage but no current promises to resolve
if (!deferred) {
// Hydrates any UI temporarily, if cache is available
if (cache.data) {
await config.cache.hydrate?.(cache);
}
return config;
}
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Detected concurrent request, waiting for it to finish'
});
}
try {
// Deferred can't reuse the value because the user's storage might clone
// or mutate the value, so we need to ask it again.
// For example with memoryStorage + cloneData
await deferred;
const state = await axios.storage.get(config.id, config);
// This is a cache mismatch and should never happen, but in case it does,
// we need to redo the request all over again.
/* c8 ignore start */
if (!state.data) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Deferred resolved, but no data was found, requesting again'
});
}
return onFulfilled!(config);
}
/* c8 ignore end */
cachedResponse = state.data;
} catch (err) {
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Deferred rejected, requesting again',
data: err
});
}
// Hydrates any UI temporarily, if cache is available
/* c8 ignore start */
if (cache.data) {
await config.cache.hydrate?.(cache);
}
/* c8 ignore end */
// The deferred is rejected when the request that we are waiting rejects its cache.
// In this case, we need to redo the request all over again.
return onFulfilled!(config);
}
} else {
cachedResponse = cache.data;
}
// The cached data is already transformed after receiving the response from the server.
// Reapplying the transformation on the transformed data will have an unintended effect.
// Since the cached data is already in the desired format, there is no need to apply the transformation function again.
config.transformResponse = undefined;
// Even though the response interceptor receives this one from here,
// it has been configured to ignore cached responses = true
config.adapter = function cachedAdapter(): Promise<CacheAxiosResponse> {
return Promise.resolve({
config,
data: cachedResponse.data,
headers: cachedResponse.headers,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
cached: true,
stale: (cache as LoadingStorageValue).previous === 'stale',
id: config.id!
});
};
if (__ACI_DEV__) {
axios.debug({
id: config.id,
msg: 'Returning cached response'
});
}
return config;
};
return {
onFulfilled
};
}