Merge pull request #1992 from automatisch/fix-proxy-configuration

fix(axios): update order of interceptors
This commit is contained in:
Ömer Faruk Aydın
2024-07-26 20:16:38 +02:00
committed by GitHub
3 changed files with 174 additions and 117 deletions

View File

@@ -3,7 +3,11 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
const config = axios.defaults; export function createInstance(customConfig = {}, { requestInterceptor, responseErrorInterceptor } = {}) {
const config = {
...axios.defaults,
...customConfig
};
const httpProxyUrl = appConfig.httpProxy; const httpProxyUrl = appConfig.httpProxy;
const httpsProxyUrl = appConfig.httpsProxy; const httpsProxyUrl = appConfig.httpsProxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl; const supportsProxy = httpProxyUrl || httpsProxyUrl;
@@ -22,7 +26,7 @@ if (supportsProxy) {
config.proxy = false; config.proxy = false;
} }
const axiosWithProxyInstance = axios.create(config); const instance = axios.create(config);
function shouldSkipProxy(hostname) { function shouldSkipProxy(hostname) {
return noProxyHosts.some(noProxyHost => { return noProxyHosts.some(noProxyHost => {
@@ -33,7 +37,7 @@ function shouldSkipProxy(hostname) {
/** /**
* The interceptors are executed in the reverse order they are added. * The interceptors are executed in the reverse order they are added.
*/ */
axiosWithProxyInstance.interceptors.request.use( instance.interceptors.request.use(
function skipProxyIfInNoProxy(requestConfig) { function skipProxyIfInNoProxy(requestConfig) {
const hostname = new URL(requestConfig.baseURL).hostname; const hostname = new URL(requestConfig.baseURL).hostname;
@@ -44,11 +48,23 @@ axiosWithProxyInstance.interceptors.request.use(
return requestConfig; return requestConfig;
}, },
undefined, (error) => Promise.reject(error)
{ synchronous: true }
); );
axiosWithProxyInstance.interceptors.request.use( // not always we have custom request interceptors
if (requestInterceptor) {
instance.interceptors.request.use(
function customInterceptor(requestConfig) {
const result = requestInterceptor.reduce((newConfig, requestInterceptor) => {
return requestInterceptor(newConfig);
}, requestConfig);
return result;
}
);
}
instance.interceptors.request.use(
function removeBaseUrlForAbsoluteUrls(requestConfig) { function removeBaseUrlForAbsoluteUrls(requestConfig) {
/** /**
* If the URL is an absolute URL, we remove its origin out of the URL * If the URL is an absolute URL, we remove its origin out of the URL
@@ -61,12 +77,25 @@ axiosWithProxyInstance.interceptors.request.use(
requestConfig.url = url.pathname + url.search; requestConfig.url = url.pathname + url.search;
return requestConfig; return requestConfig;
} catch { } catch (err) {
return requestConfig; return requestConfig;
} }
}, },
undefined, (error) => Promise.reject(error)
{ synchronous: true}
); );
export default axiosWithProxyInstance;
// not always we have custom response error interceptor
if (responseErrorInterceptor) {
instance.interceptors.response.use(
(response) => response,
responseErrorInterceptor
);
}
return instance;
}
const defaultInstance = createInstance();
export default defaultInstance;

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, it, expect, vi } from 'vitest'; import { beforeEach, describe, it, expect, vi } from 'vitest';
describe('Axios with proxy', () => { describe('Custom default axios with proxy', () => {
beforeEach(() => { beforeEach(() => {
vi.resetModules(); vi.resetModules();
}); });
@@ -23,7 +23,13 @@ describe('Axios with proxy', () => {
expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls'); expect(secondRequestInterceptor.fulfilled.name).toBe('removeBaseUrlForAbsoluteUrls');
}); });
describe('skipProxyIfInNoProxy', () => { it('should throw with invalid url (consisting of path alone)', async () => {
const axios = (await import('./axios-with-proxy.js')).default;
await expect(() => axios('/just-a-path')).rejects.toThrowError('Invalid URL');
});
describe('with skipProxyIfInNoProxy interceptor', () => {
let appConfig, axios; let appConfig, axios;
beforeEach(async() => { beforeEach(async() => {
appConfig = (await import('../config/app.js')).default; appConfig = (await import('../config/app.js')).default;
@@ -67,7 +73,7 @@ describe('Axios with proxy', () => {
}); });
}); });
describe('removeBaseUrlForAbsoluteUrls', () => { describe('with removeBaseUrlForAbsoluteUrls interceptor', () => {
let axios; let axios;
beforeEach(async() => { beforeEach(async() => {
axios = (await import('./axios-with-proxy.js')).default; axios = (await import('./axios-with-proxy.js')).default;
@@ -116,4 +122,48 @@ describe('Axios with proxy', () => {
expect(interceptedRequestConfig.url).toBe('/path?query=1'); expect(interceptedRequestConfig.url).toBe('/path?query=1');
}); });
}); });
describe('with extra requestInterceptors', () => {
it('should apply extra request interceptors in the middle', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.test = true;
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
const requestInterceptors = instance.interceptors.request.handlers;
const customInterceptor = requestInterceptors[1].fulfilled;
expect(requestInterceptors.length).toBe(3);
expect(customInterceptor({})).toStrictEqual({ test: true });
});
it('should work with a custom interceptor setting a baseURL and a request to path', async () => {
const { createInstance } = await import('./axios-with-proxy.js');
const interceptor = (config) => {
config.baseURL = 'http://localhost';
return config;
}
const instance = createInstance({}, {
requestInterceptor: [
interceptor
]
});
try {
await instance.get('/just-a-path');
} catch (error) {
expect(error.config.baseURL).toBe('http://localhost');
expect(error.config.url).toBe('/just-a-path');
}
})
});
}); });

View File

@@ -1,41 +1,8 @@
import HttpError from '../../errors/http.js'; import HttpError from '../../errors/http.js';
import axios from '../axios-with-proxy.js'; import { createInstance } from '../axios-with-proxy.js';
// Mutates the `toInstance` by copying the request interceptors from `fromInstance`
const copyRequestInterceptors = (fromInstance, toInstance) => {
// Copy request interceptors
fromInstance.interceptors.request.forEach(interceptor => {
toInstance.interceptors.request.use(
interceptor.fulfilled,
interceptor.rejected,
{
synchronous: interceptor.synchronous,
runWhen: interceptor.runWhen
}
);
});
}
export default function createHttpClient({ $, baseURL, beforeRequest = [] }) { export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
const instance = axios.create({ async function interceptResponseError(error) {
baseURL,
});
// 1. apply the beforeRequest functions from the app
instance.interceptors.request.use((requestConfig) => {
const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => {
return beforeRequestFunc($, newConfig);
}, requestConfig);
return result;
});
// 2. inherit the request inceptors from the parent instance
copyRequestInterceptors(axios, instance);
instance.interceptors.response.use(
(response) => response,
async (error) => {
const { config, response } = error; const { config, response } = error;
// Do not destructure `status` from `error.response` because it might not exist // Do not destructure `status` from `error.response` because it might not exist
const status = response?.status; const status = response?.status;
@@ -58,8 +25,19 @@ export default function createHttpClient({ $, baseURL, beforeRequest = [] }) {
} }
throw new HttpError(error); throw new HttpError(error);
};
const instance = createInstance(
{
baseURL,
},
{
requestInterceptor: beforeRequest.map((originalBeforeRequest) => {
return (requestConfig) => originalBeforeRequest($, requestConfig);
}),
responseErrorInterceptor: interceptResponseError,
} }
); )
return instance; return instance;
} }