Skip to content

feat(browse): BROWSE_NO_PROXY env var to bypass system proxy on launch#1037

Open
samque1983 wants to merge 2 commits intogarrytan:mainfrom
samque1983:fix/browse-macos-system-proxy-bypass
Open

feat(browse): BROWSE_NO_PROXY env var to bypass system proxy on launch#1037
samque1983 wants to merge 2 commits intogarrytan:mainfrom
samque1983:fix/browse-macos-system-proxy-bypass

Conversation

@samque1983
Copy link
Copy Markdown

Problem

On macOS, Chromium (via Playwright) inherits the system HTTP/S proxy by default. When the user runs a local VPN/proxy app that does TLS interception (Shadowrocket, ClashX, Surge, V2rayU, etc.), Chromium's HTTPS handshake breaks:

ERROR:net/socket/ssl_client_socket_impl.cc:918 handshake failed;
returned -1, SSL error code 1, net_error -100

Every browse goto then returns ERR_ABORTED, the renderer crashes, and the browse server exits — triggering the familiar [browse] Starting server... restart loop on every subsequent command.

This is particularly confusing because curl continues to work fine (curl ignores the system proxy by default), so health checks and CI probes pass while browse is completely broken.

Repro

# macOS with Shadowrocket / ClashX / similar running in HTTP-proxy mode
# (scutil --proxy shows HTTPProxy: 127.0.0.1:<port>)

browse goto https://example.com
# → Timeout 15000ms exceeded, then ERR_ABORTED / net_error -100

BROWSE_NO_PROXY=1 browse goto https://example.com
# → Navigated to https://example.com (200)

Verified locally: chrome-headless-shell --proxy-server="http://127.0.0.1:1082" --dump-dom https://example.com reproduces the SSL error; adding --proxy-server="direct://" fixes it.

Solution

Opt-in env var BROWSE_NO_PROXY=1 passes --proxy-server=direct:// to Chromium at launch, bypassing the system proxy entirely.

  • Default behavior unchanged: still inherits the system proxy, so corporate proxy users are unaffected.
  • No new flags at the CLI layer: env-var only, matches the pattern of existing BROWSE_EXTENSIONS_DIR.
  • 13 lines including docstring explaining why curl succeeds while browse fails (the part that threw me off when diagnosing).

Test plan

  • Repro fail without env var: browse goto https://example.comTimeout 15000ms / ERR_ABORTED
  • Fix succeeds with env var: BROWSE_NO_PROXY=1 browse goto https://example.com → 200, snapshot works
  • Default path unchanged: launchArgs stays empty when env var unset, so corporate proxy flow is untouched
  • Reviewer verify on a machine with no system proxy — should be a no-op

quetaifu added 2 commits April 17, 2026 10:31
On macOS, Chromium (via Playwright) inherits the system HTTP/S proxy by
default. When the user runs a local VPN/proxy app (Shadowrocket, ClashX,
Surge, etc.) that does TLS interception, Chromium's HTTPS handshake
breaks with SSL error 1 / net_error -100, and every goto fails with
ERR_ABORTED. The renderer then crashes and kills the browse server,
triggering the familiar '[browse] Starting server...' restart loop.

curl is unaffected because curl ignores the system proxy by default,
so curl-based health checks continue to pass while browse is broken —
making this failure mode especially confusing.

Opt-in: set BROWSE_NO_PROXY=1 in the environment and browse will pass
'--proxy-server=direct://' to Chromium at launch, bypassing the system
proxy entirely. Default behavior unchanged (still inherits system proxy)
so corporate proxy users are not affected.

Repro:
  # With Shadowrocket running in HTTP-proxy mode on macOS:
  browse goto https://example.com        # ERR_ABORTED / Timeout 15000ms
  BROWSE_NO_PROXY=1 browse goto https://example.com   # 200 OK
When a transparent-proxy VPN (Shadowrocket, ClashX, Surge in "enhanced/TUN
mode") captures traffic at the routing layer via a utun interface, the
original opt-in patch (BROWSE_NO_PROXY=1 → --proxy-server=direct://)
becomes counter-productive: direct:// can't escape TUN, packets still get
MITM'd, and Chromium's own cert store rejects the VPN's MITM cert
resulting in SSL error code 1, net_error -100 on every HTTPS navigation.

The fix in HTTP-proxy-only mode (original scenario) and the fix in TUN
mode need opposite Chromium configs:
  - HTTP-proxy-only: --proxy-server=direct:// bypasses the proxy.
  - TUN mode: no flag — Chromium inherits system proxy, connects through
    the VPN's HTTPS proxy explicitly, and accepts the MITM cert signed
    by a keychain-trusted root.

Heuristic: look for a utun interface with an IP in 198.18.x.x (IANA
benchmark range, used by Shadowrocket/ClashX for TUN routing). When
found, skip direct:// and log an explanation so the user knows why.

Backward compatible:
  - BROWSE_NO_PROXY unset (default): no change, no flag added.
  - BROWSE_NO_PROXY=1 + no TUN: no change, direct:// applied as before.
  - BROWSE_NO_PROXY=1 + TUN: NEW — auto-skip with informative log.
  - BROWSE_NO_PROXY=force: always apply direct:// (escape hatch for
    users who want the old strict opt-in behavior).

Verified on macOS with Shadowrocket TUN mode active (utun6 198.18.0.1):
before patch → SSL handshake failed / net_error -100 on all HTTPS.
After patch → example.com + authenticated fly.dev pages load correctly.
@samque1983
Copy link
Copy Markdown
Author

Pushed a follow-up commit (e695f7f) that auto-detects VPN TUN mode so BROWSE_NO_PROXY=1 doesn't backfire when the user's VPN app switches interception mode.

Problem: Shadowrocket/ClashX/Surge have two modes:

  1. HTTP-proxy-only (the original scenario this PR solved): --proxy-server=direct:// bypasses the proxy and TLS works.
  2. TUN/enhanced mode: captures all traffic at the routing layer via a utun interface in 198.18.0.0/15. direct:// no longer escapes — packets still get MITM'd and Chromium rejects the MITM cert. Oddly, the fix in this mode is the opposite of the original: don't set direct:// so Chromium inherits the system proxy, connects through the VPN's HTTPS proxy explicitly, and accepts the keychain-trusted MITM cert.

Users who set export BROWSE_NO_PROXY=1 in their shell rc get silently bitten when their VPN app switches modes.

The patch: when BROWSE_NO_PROXY=1 is set AND a utun with inet 198.18.x.x exists, skip --proxy-server=direct:// and log an explanation. All other paths unchanged. A BROWSE_NO_PROXY=force escape hatch restores strict opt-in behavior for anyone who wants it.

Backward compat:

  • BROWSE_NO_PROXY unset (default): unchanged — no flag.
  • BROWSE_NO_PROXY=1 + no TUN: unchanged — direct:// applied.
  • BROWSE_NO_PROXY=1 + TUN: NEW — auto-skip + log.
  • BROWSE_NO_PROXY=force: always direct://.

Verified 2026-04-19 on macOS with Shadowrocket TUN mode active (utun6 198.18.0.1): before → SSL handshake failed / net_error -100 on all HTTPS; after → loads cleanly.

Happy to split this into a separate PR if it's easier to review — let me know what you prefer.

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.

1 participant