Files
seerr/server/utils/customProxyAgent.test.ts
fallenbagel 84accb544a fix(proxy): proxy clients built before proxy init
Proxying for axios relied on the per-instance httpAgent/httpsAgent, which axios.create() snapshots
from axios.defaults at construction time. API clients constructed at import (such as
BaseScanner.tmdb, avalabilitySync.tmdb) are built before createCustomProxyAgent runs, so they
snapshotted no proxy agent and bypassed the proxy permanently, while clients created per-request
afterwards were proxied. This fix resolves the agent per request from a live module-level holder via
a stable interceptor that is registered on the default instance and every ExternalAPI client, so
construction order no longer matters. forceIpv4First has been moved to the same path logic as well.

fix #3193
2026-06-23 10:13:06 +08:00

73 lines
2.1 KiB
TypeScript

import ExternalAPI from '@server/api/externalapi';
import type { ProxySettings } from '@server/lib/settings';
import createCustomProxyAgent from '@server/utils/customProxyAgent';
import axios, {
type AxiosResponse,
type InternalAxiosRequestConfig,
} from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
class TestExternalAPI extends ExternalAPI {
public constructor() {
super('https://api.themoviedb.org/3', {}, {});
}
public async resolvedHttpsAgent(path = '/movie/123'): Promise<unknown> {
let captured: InternalAxiosRequestConfig | undefined;
this.axios.defaults.adapter = (config) => {
captured = config;
return Promise.resolve({
data: {},
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
} as AxiosResponse);
};
await this.axios.get(path);
return captured?.httpsAgent;
}
}
// constructed before any test configures the proxy, like BaseScanner.tmdb
const preProxyClient = new TestExternalAPI();
const proxySettings: ProxySettings = {
enabled: true,
hostname: 'proxy.test',
port: 3128,
useSsl: false,
user: '',
password: '',
bypassFilter: '*.bypass.test',
bypassLocalAddresses: true,
};
describe('proxy routing (construction-order independence)', () => {
beforeEach(() => {
mock.method(axios, 'head', async () => ({ status: 200 }));
});
afterEach(() => {
mock.restoreAll();
});
it('routes a client constructed BEFORE the proxy was configured', async () => {
await createCustomProxyAgent(proxySettings);
const agent = await preProxyClient.resolvedHttpsAgent();
assert.ok(
agent instanceof HttpsProxyAgent,
'client created before proxy setup must still route through the proxy'
);
});
it('routes a client constructed AFTER the proxy was configured', async () => {
await createCustomProxyAgent(proxySettings);
const agent = await new TestExternalAPI().resolvedHttpsAgent();
assert.ok(agent instanceof HttpsProxyAgent);
});
});