1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-07 14:55:27 +02:00

fix(caching): integrate with axios cache interceptor [EE-6505] (#10922)

* integrate with axios-cache-interceptor
* remove extra headers as not needed
This commit is contained in:
Prabhat Khera 2024-01-11 11:12:53 +13:00 committed by GitHub
parent d0b9e3a732
commit 0b9cebc685
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 55 deletions

View file

@ -1,8 +1,18 @@
import axiosOrigin, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { setupCache } from 'axios-cache-adapter';
import Axios, {
AxiosError,
AxiosInstance,
InternalAxiosRequestConfig,
} from 'axios';
import {
setupCache,
buildMemoryStorage,
CacheAxiosResponse,
InterpreterResult,
AxiosCacheInstance,
} from 'axios-cache-interceptor';
import { loadProgressBar } from 'axios-progress-bar';
import 'axios-progress-bar/dist/nprogress.css';
import PortainerError from '@/portainer/error';
import {
@ -12,55 +22,82 @@ import {
portainerAgentTargetHeader,
} from './http-request.helper';
export const cache = setupCache({
maxAge: CACHE_DURATION,
debug: false, // set to true to print cache hits/misses
exclude: {
query: false, // include urls with query params
methods: ['put', 'patch', 'delete'],
filter: (req: InternalAxiosRequestConfig) => {
// exclude caching get requests unless the path contains 'kubernetes'
if (!req.url?.includes('kubernetes') && req.method === 'get') {
return true;
}
const portainerCacheHeader = 'X-Portainer-Cache';
const acceptHeader = req.headers?.Accept;
if (
acceptHeader &&
typeof acceptHeader === 'string' &&
acceptHeader.includes('application/yaml')
) {
return true;
}
// exclude caching post requests unless the path contains 'selfsubjectaccessreview'
if (
!req.url?.includes('selfsubjectaccessreview') &&
req.method === 'post'
) {
return true;
}
return false;
const storage = buildMemoryStorage();
// mock the cache adapter
export const cache = {
store: {
clear: () => {
storage.data = {};
},
},
// ask to clear cache on mutation
invalidate: async (_, req) => {
dispatchCacheRefreshEventIfNeeded(req);
},
};
function headerInterpreter(
headers?: CacheAxiosResponse['headers']
): InterpreterResult {
if (!headers) {
return 'not enough headers';
}
if (headers[portainerCacheHeader]) {
return CACHE_DURATION;
}
return 'not enough headers';
}
const axios = Axios.create({ baseURL: 'api' });
axios.interceptors.request.use((req) => {
dispatchCacheRefreshEventIfNeeded(req);
return req;
});
// by default don't use the cache adapter
const axios = axiosOrigin.create({ baseURL: 'api' });
// type guard the axios instance
function isAxiosCacheInstance(
a: AxiosInstance | AxiosCacheInstance
): a is AxiosCacheInstance {
return (a as AxiosCacheInstance).defaults.cache !== undefined;
}
// when entering a kubernetes environment, or updating user settings, update the cache adapter
export function updateAxiosAdapter(useCache: boolean) {
axios.defaults.adapter = useCache ? cache.adapter : undefined;
if (useCache) {
if (isAxiosCacheInstance(axios)) {
return;
}
setupCache(axios, {
storage,
ttl: CACHE_DURATION,
methods: ['get', 'head', 'options', 'post'],
// cachePredicate determines if the response should be cached based on response
cachePredicate: {
containsHeaders: {
[portainerCacheHeader]: () => true,
},
ignoreUrls: [/^(?!.*\bkubernetes\b).*$/gm],
responseMatch: (res) => {
if (res.config.method === 'post') {
if (res.config.url?.includes('selfsubjectaccessreviews')) {
return true;
}
return false;
}
return true;
},
},
// headerInterpreter interprets the response headers to determine if the response should be cached
headerInterpreter,
});
}
}
loadProgressBar(undefined, axios);
export default axios;
loadProgressBar(undefined, axios);
export const agentTargetHeader = 'X-PortainerAgent-Target';
export function agentInterceptor(config: InternalAxiosRequestConfig) {
@ -173,7 +210,7 @@ export function isDefaultResponse(
export function isAxiosError<ResponseType>(
error: unknown
): error is AxiosError<ResponseType> {
return axiosOrigin.isAxiosError(error);
return Axios.isAxiosError(error);
}
export function arrayToJson<T>(arr?: Array<T>) {

View file

@ -1,4 +1,5 @@
import { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { CacheAxiosResponse } from 'axios-cache-interceptor';
import { IHttpResponse } from 'angular';
import axios from './axios';
@ -8,7 +9,9 @@ axios.interceptors.request.use(csrfInterceptor);
let csrfToken: string | null = null;
export function csrfTokenReaderInterceptor(config: AxiosResponse) {
export function csrfTokenReaderInterceptor(
config: CacheAxiosResponse | AxiosResponse
) {
const csrfTokenHeader = config.headers['x-csrf-token'];
if (csrfTokenHeader) {
csrfToken = csrfTokenHeader;

View file

@ -216,6 +216,14 @@ async function getApplicationByKind<
buildUrl(environmentId, namespace, `${appKind}s`, name),
{
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
// this logic is to get the latest YAML response
// axios-cache-adapter looks for the response headers to determine if the response should be cached
// to avoid writing requestInterceptor, adding a query param to the request url
params: yaml
? {
_: Date.now(),
}
: null,
}
);
return data;