Skip to content

fix(proxy): proxy clients built before proxy init#3196

Merged
fallenbagel merged 2 commits into
developfrom
fix/proxy-bypass-import-time-clients
Jun 24, 2026
Merged

fix(proxy): proxy clients built before proxy init#3196
fallenbagel merged 2 commits into
developfrom
fix/proxy-bypass-import-time-clients

Conversation

@fallenbagel

@fallenbagel fallenbagel commented Jun 23, 2026

Copy link
Copy Markdown
Member

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, and axios is only proxied via the per-instance httpAgent/httpsAgent. axios.create() snapshots axios.defaults when 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. createCustomProxyAgent runs in the async startup block in server/index.ts, but some clients are built earlier at module-import time, specifically the tmdb field on BaseScanner (which is shared by the Sonarr, Radarr, Plex, and Jellyfin scanners) and on availabilitySync, 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.

forceIpv4First had the same latent defect (it set agents on axios.defaults in 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?

  • I confirmed this with a test rather than by testing it manually.
    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.
image

After this fix is applied the test passes:
image

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

Summary by CodeRabbit

Release Notes

  • New Features / Refactor

    • Unified proxy request handling across outgoing Axios requests using a shared interceptor.
    • Centralized “force IPv4 first” behavior so it’s applied at startup and respects proxy bypass rules.
  • Bug Fixes

    • Improved proxy routing consistency regardless of when HTTP clients are created.
  • Tests

    • Added coverage to verify proxy routing is independent of client construction order and that connectivity checks behave correctly.

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
@fallenbagel fallenbagel requested a review from Copilot June 23, 2026 02:28
@fallenbagel fallenbagel requested a review from a team as a code owner June 23, 2026 02:28
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR centralizes Axios proxy handling by replacing per-instance interceptor registration with a single module-level proxyRequestInterceptor in customProxyAgent.ts. IPv4-first forcing is extracted into a setForceIpv4First helper. All Axios consumers (ExternalAPI, TautulliAPI, ImageProxy) switch to the new shared interceptor, and server/index.ts uses the new helper. A new test suite verifies proxy routing consistency regardless of construction order.

Changes

Proxy Interceptor Centralization

Layer / File(s) Summary
Module-level proxy state and proxyRequestInterceptor
server/utils/customProxyAgent.ts
Introduces ProxyState, proxyState, ipv4Agents, setForceIpv4First, and proxyRequestInterceptor at module scope. Registers the interceptor once on the default Axios instance. Updates createCustomProxyAgent to assign/clear proxyState instead of setting axios.defaults.* and registering a new interceptor per call. Removes the previously exported requestInterceptorFunction. Simplifies connectivity-check failure path by not redundantly restoring global dispatcher.
Server startup IPv4-first wiring
server/index.ts
Removes direct axios, http, and https imports used to force IPv4. Imports setForceIpv4First alongside createCustomProxyAgent and calls setForceIpv4First(settings.network.forceIpv4First) after settings load.
Axios consumer interceptor updates
server/api/externalapi.ts, server/api/tautulli.ts, server/lib/imageproxy.ts
Updates imports and interceptors.request.use(...) calls in ExternalAPI, TautulliAPI, and ImageProxy constructors from requestInterceptorFunction to proxyRequestInterceptor.
Proxy routing test suite
server/utils/customProxyAgent.test.ts
New test file with a TestExternalAPI adapter override that captures the resolved httpsAgent. Asserts that both clients constructed before and after createCustomProxyAgent is called route through HttpsProxyAgent.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #3193: Proxy configuration not consistently applied — This PR directly addresses the root cause: per-instance interceptor registration meant Axios clients created before createCustomProxyAgent was called did not inherit proxy settings; the new module-level proxyRequestInterceptor ensures all instances are covered regardless of construction order.

🐇 A hop to the left, a hop to the right,
One interceptor now rules the night!
No more agents slipping through,
Every request gets proxied too.
proxyState holds the master plan —
IPv4 or proxy, I'm the rabbit fan! 🌐✨

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing proxy routing for clients constructed before proxy initialization.
Linked Issues check ✅ Passed The PR fully addresses issue #3193 by implementing per-request proxy resolution, ensuring all HTTP/HTTPS traffic routes through the configured proxy regardless of client construction timing.
Out of Scope Changes check ✅ Passed All changes directly address the proxy routing issue: refactoring interceptor architecture, adding the setForceIpv4First helper, updating imports across proxy-related modules, and adding validation tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread server/utils/customProxyAgent.test.ts Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 proxyRequestInterceptor that 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.

coderabbitai[bot]

This comment was marked as outdated.

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.

@gauthier-th gauthier-th left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@seerr-automation-bot seerr-automation-bot added this to the v3.4.0 milestone Jun 24, 2026
@fallenbagel fallenbagel enabled auto-merge (squash) June 24, 2026 07:44
@fallenbagel fallenbagel merged commit 8ad191f into develop Jun 24, 2026
25 checks passed
@fallenbagel fallenbagel deleted the fix/proxy-bypass-import-time-clients branch June 24, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proxy configuration not consistently applied (some outbound requests bypass proxy)

6 participants