fix(proxy): proxy clients built before proxy init#3196
Conversation
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
📝 WalkthroughWalkthroughThe PR centralizes Axios proxy handling by replacing per-instance interceptor registration with a single module-level ChangesProxy Interceptor Centralization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes inconsistent proxy routing by making proxy (and force-IPv4) agent selection happen at request time via a stable Axios request interceptor, ensuring Axios clients constructed before proxy initialization still honor the configured proxy (addressing #3193).
Changes:
- Introduces a mutable proxy/IPv4 agent state and a shared
proxyRequestInterceptorthat resolves agents per request. - Registers the interceptor on the default Axios instance and on
axios.create()clients (ExternalAPI, ImageProxy, Tautulli). - Adds a unit test that verifies proxy routing works for clients constructed before and after proxy setup.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| server/utils/customProxyAgent.ts | Moves proxy + force-IPv4 handling to per-request agent resolution via a shared Axios interceptor and mutable module state. |
| server/utils/customProxyAgent.test.ts | Adds a construction-order regression test to ensure pre-init Axios clients still route through the proxy. |
| server/lib/imageproxy.ts | Switches ImageProxy’s Axios client to use the new shared proxy request interceptor. |
| server/index.ts | Uses setForceIpv4First() during startup and keeps proxy initialization in the async startup flow. |
| server/api/tautulli.ts | Switches Tautulli API Axios client to use the new shared proxy request interceptor. |
| server/api/externalapi.ts | Switches ExternalAPI Axios client to use the new shared proxy request interceptor (covers scanners and other external calls). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The health-check catch reset undicit to defaultAgent while proxyState stayed set for axios, so the two transports disagreed after a transient failure where axios kept proxying, undici when direct. This drops the reset so both stay on the proxy, matching the proxy-only egress intent. This was pre-existing on develop too where axios.defaults stayed on the proxy while undici was reset. This just makes both the paths consistent.
Description
TLDR; proxy was only being applied to axios clients created after startup. The scanners' TMDB client is created at import, before the proxy is set up, so it never got it and went direct connection. (This is for @gauthier-th 's benefit)
When a proxy is configured under Settings > Network, only part of Seerr's outbound traffic went through it. Plex and anything issued per-request after startup were proxied, but TMDB lookups during library scans, IMDb ratings, and similar requests bypassed the proxy and went straight out. That breaks setups with a strict outbound firewall and produces failed scans with nothing in the proxy logs.
The issue is the construction timing. All outbound HTTP goes through
axios, andaxiosis only proxied via the per-instancehttpAgent/httpsAgent.axios.create()snapshotsaxios.defaultswhen the instance is built and never re-reads it, so an instance created before the proxy is configured carries no proxy agent for its whole lifetime.createCustomProxyAgentruns in the async startup block inserver/index.ts, but some clients are built earlier at module-import time, specifically thetmdbfield onBaseScanner(which is shared by the Sonarr, Radarr, Plex, and Jellyfin scanners) and onavailabilitySync, since those singletons sit at the top of the import graph. They snapshot an empty agent and bypass the proxy forever. Clients created per request after startup inherit the proxy, which is why only some traffic showed up in the logs.This PR fixes this by not binding the proxy to the axios instance at construction. Proxy config now lives in a mutable module-level holder, and one stable interceptor resolves the agent per request from that holder. It's registered once on the default axios instance and once per ExternalAPI client, replacing the old late-bound interceptor that was undefined for any client built before setup. Since the agent is chosen at request time, an instance built before the proxy is configured routes through it as soon as it's enabled.
forceIpv4Firsthad the same latent defect (it set agents onaxios.defaultsin the same async block), so it moves onto the same per-request resolution. Bypass-filter and local-address behavior are unchanged, env-var proxying is disabled when the in-app proxy is active, and on a proxy health-check failure the proxy state is kept instead of silently falling back to a blocked direct connection.One thing the unit test doesn't cover is live transport through an axios-rate-limit client. The interceptor runs before axios-rate-limit wraps the adapter, so the resolved agent is preserved, but I checked that separately against a local proxy instead of adding a port-binding, timing-sensitive test to the suite.
How Has This Been Tested?
It builds an ExternalAPI client at import time, before the proxy is configured, then detects proxying by swapping in a stub axios adapter and reading the exact agent axios would hand to the transport, so it needs no network.
On develop the test fails. The pre-existing client resolves no proxy agent and would go direct. A second client built after the proxy is configured passes on develop, ruling out a test that just always fails and showing the post-startup path was never broken.

After this fix is applied the test passes:

preview-proxy-bypass-fix. This was tested by the OP of the bug report (see, Proxy configuration not consistently applied (some outbound requests bypass proxy) #3193 (comment))Screenshots / Logs (if applicable)
Checklist:
pnpm buildpnpm i18n:extractSummary by CodeRabbit
Release Notes
New Features / Refactor
Bug Fixes
Tests