Skip to content

🤖 feat: proxy localhost URLs through Coder wildcard DNS in browser mode#2797

Merged
ethanndickson merged 7 commits into
mainfrom
proxy-router-rbbz
Mar 6, 2026
Merged

🤖 feat: proxy localhost URLs through Coder wildcard DNS in browser mode#2797
ethanndickson merged 7 commits into
mainfrom
proxy-router-rbbz

Conversation

@ethanndickson
Copy link
Copy Markdown
Member

@ethanndickson ethanndickson commented Mar 5, 2026

Summary

When Mux runs inside a Coder workspace, clicking a localhost:PORT link anywhere in the app now transparently opens through the Coder wildcard proxy URL instead. Works zero-config in browser mode by deriving the proxy template from the current hostname (e.g. 5173--dev--ws--user--apps.example.com{{port}}--dev--ws--user--apps.example.com), and also supports explicit VSCODE_PROXY_URI/MUX_PROXY_URI env vars for Electron mode.

Closes #2376

Background

Coder's web terminal and code-server both rewrite localhost URLs to proxy URLs so that clicking http://localhost:3000 in a remote workspace opens the correct externally-reachable URL. Mux lacked this — users in Coder environments had to manually construct proxy URLs.

Key insight from upstream: VSCODE_PROXY_URI is only injected by Coder into VS Code extension host processes, not into plain SSH shells. So env-var-only detection is insufficient for browser mode. code-server solves this by deriving the template from window.location; Coder's web terminal gets it from the /api/v2/regions API. We take the simpler code-server-like approach: if the browser hostname looks like a Coder port-forward URL (<port>--<segments>.<domain>), derive the template by replacing the port prefix with {{port}}.

Implementation

Architecture: one shared pure normalizer + thin adapters at each navigation boundary + browser-side auto-detection.

Layer File(s) What it does
Shared normalizer src/common/utils/localhostProxyUrl.ts Pure function: rewrites loopback URLs using {{port}}/{{host}} template. Supports both subdomain-based and path-based proxy templates (preserves template path prefix). Preserves path/query/hash. Falls back to original URL on any failure.
Browser auto-detect src/browser/utils/browserLocalhostProxyTemplate.ts Derives proxy template from window.location.hostname when no injected template exists. Detects Coder-style <port>--<suffix>.<domain> hostnames and synthesizes {{port}}--<suffix>.<domain>.
Electron src/desktop/main.ts, src/desktop/terminalWindowManager.ts setWindowOpenHandler + will-navigate pass URLs through normalizer before shell.openExternal().
Terminal env src/node/services/terminalService.ts PTY sessions inherit VSCODE_PROXY_URI and MUX_PROXY_URI so CLI tools can use them.
Browser mode src/node/orpc/server.ts, src/browser/utils/windowOpenLocalhostProxy.ts Server injects window.__MUX_PROXY_URI_TEMPLATE__ into SPA shell (static middleware skips index.html so the SPA fallback is the single source of truth for HTML delivery). window.open wrapper normalizes loopback URLs.
Markdown links src/browser/features/Messages/MarkdownComponents.tsx Chat anchor renderer normalizes href via the same shared policy.

Template resolution order (browser mode):

  1. Server-injected window.__MUX_PROXY_URI_TEMPLATE__ (from MUX_PROXY_URI > VSCODE_PROXY_URI env vars)
  2. Auto-derived from window.location.hostname if it matches Coder's <port>--... pattern
  3. No rewrite (original URL returned unchanged)

Template resolution order (Electron): MUX_PROXY_URI > VSCODE_PROXY_URI > no rewrite.

Validation

  • Unit tests for normalizer (14 cases: loopback variants, path/query/hash preservation, path-based proxy templates, template edge cases, IPv6, default ports)
  • Unit tests for browser template resolver (5 cases: injected preference, Coder hostname derivation, port preservation, non-Coder passthrough, non-HTTP passthrough)
  • Markdown component test for Coder auto-detect (no injected template + Coder-style hostname → correct rewrite)
  • Terminal service tests for env propagation
  • Server tests for HTML template injection
  • make static-check passes

Risks

Low. All rewrite paths fall back to the original URL on any failure. The Coder hostname auto-detection is conservative — it only activates when the first DNS label is <digits>--<at-least-one-segment>, which is specific to Coder's wildcard proxy format.


Generated with mux • Model: anthropic:claude-opus-4-6 • Thinking: xhigh

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e998de11af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/localhostProxyUrl.ts Outdated
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

Re: the P1 concern about rewrittenUrl.pathname = sourceUrl.pathname dropping proxy template path prefixes (e.g. https://coder.example/proxy/{{port}}/):

I investigated both upstream repos (coder/coder and coder/code-server) to verify whether path-based proxy templates are a real production scenario:

Coder platform: VSCODE_PROXY_URI is always subdomain-based (https://{{port}}--agent--ws--user.apps.example.com) or empty. The vscodeProxyURI() function in coderd/agentapi/manifest.go explicitly returns "" when wildcard subdomains are not configured — there is no code path that produces a path-based template. The agent code even comments: "If this is empty string, do not set anything. Code-server auto defaults using its basepath to construct a path based port proxy."

code-server: While it does support path-based templates internally (./proxy/{{port}}), its own resolveExternalUri also replaces the entire URI with the rendered template — it does NOT preserve the source path either. Clicking http://localhost:3000/foo in code-server resolves to just /proxy/3000/, dropping /foo.

Conclusion: Since Coder never produces path-based templates for VSCODE_PROXY_URI, and MUX_PROXY_URI is our own env var whose format we control, the current behavior is correct for all real-world scenarios. Our approach of preserving the source path actually provides better UX than code-server — http://localhost:3000/foohttps://3000--slug.example.com/foo instead of losing the path.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e998de11af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Messages/MarkdownComponents.tsx
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

Both prior Codex review comments were investigated by cloning the upstream coder/coder and coder/code-server repos. Both were false alarms:


P1: "Preserve proxy template path when rewriting localhost URLs"
Concern: rewrittenUrl.pathname = sourceUrl.pathname drops path prefixes from templates like https://coder.example/proxy/{{port}}/.

Why this is wrong:

  1. Coder never produces path-based templates. VSCODE_PROXY_URI is always subdomain-based (https://{{port}}--agent--ws--user.apps.example.com) or empty. The vscodeProxyURI() function in coderd/agentapi/manifest.go explicitly returns "" when wildcard subdomains are not configured. The agent code comments: "If this is empty string, do not set anything. Code-server auto defaults using its basepath to construct a path based port proxy."
  2. code-server's own resolveExternalUri also drops the source path. It replaces the entire URI with the rendered template — http://localhost:3000/foo resolves to just /proxy/3000/, dropping /foo. Our behavior of preserving the source path is actually better UX.
  3. MUX_PROXY_URI is our own env var whose format we control and document.

P2: "Pass hostname, not host, into localhost proxy normalization"
Concern: window.location.host includes the UI port (e.g. :8443), breaking {{host}} templates.

Why this is wrong:

  1. Coder's VSCODE_PROXY_URI never contains {{host}} — the hostname is fully pre-baked in the subdomain pattern. The browserHost value is never substituted in real Coder environments.
  2. code-server uses window.location.host (with port).replace('{{host}}', window.location.host) in patches/proxy-uri.diff. Our MarkdownComponents.tsx already matched upstream.
  3. Including the port is more correct — if the UI runs on a non-default port, the proxy should route through it.

The one valid observation was an internal inconsistency: windowOpenLocalhostProxy.ts used hostname while MarkdownComponents.tsx used host. This is now fixed in commit 279f21a — both use window.location.host, matching code-server. Also cleaned up a redundant type cast in MarkdownComponents.tsx.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 279f21a3db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/localhostProxyUrl.ts Outdated
Comment thread src/desktop/main.ts Outdated
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

All review threads across all rounds are resolved. Summary:

Round 1:

  • P1 (pathname preservation): Dismissed with evidence — Coder never produces path-based VSCODE_PROXY_URI templates (always subdomain-based), and code-server's own resolveExternalUri also drops the source path. No real-world scenario where template pathname matters.
  • P2 (hostname vs host): Investigated upstream — code-server uses window.location.host (with port). Fixed the one inconsistent caller (windowOpenLocalhostProxy.ts) to align with host. Removed redundant Window cast in MarkdownComponents.tsx since global.d.ts already declares the type.

Round 2:

  • P2 duplicate (pathname preservation again): Exact repeat of round 1 P1. Already dismissed twice with upstream evidence.
  • P3 (empty-string env var fallback): Valid concern — fixed in 40ea837. All 3 callers (main.ts, terminalWindowManager.ts, terminalService.ts) now use .trim() || ... instead of bare ??, matching the pattern already used in server.ts:getBrowserProxyUriTemplate(). Empty/whitespace-only MUX_PROXY_URI now correctly falls through to VSCODE_PROXY_URI.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ethanndickson ethanndickson changed the title 🤖 feat: normalize localhost URLs via proxy template for Coder environments 🤖 feat: proxy localhost URLs through Coder wildcard DNS in browser mode Mar 5, 2026
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79d52244c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/localhostProxyUrl.ts Outdated
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

All review threads across all three rounds are resolved. Full accounting:

Round 1 (2 threads):

  • P1 — "Preserve proxy template path when rewriting localhost URLs" (PRRT_kwDOPxxmWM5yNiHD): Dismissed. Investigated upstream by cloning coder/coder and coder/code-server — Coder never produces path-based VSCODE_PROXY_URI templates (always subdomain-based {{port}}--...), and code-server's own resolveExternalUri also drops the source path. No real-world template has a meaningful pathname to preserve.
  • P2 — "Pass hostname, not host, into localhost proxy normalization" (PRRT_kwDOPxxmWM5yNpXc): Fixed in 279f21a. Aligned both browser callers to window.location.host (matching code-server's approach). Removed redundant Window cast.

Round 2 (2 threads):

  • P2 — "Keep proxy template pathname" (PRRT_kwDOPxxmWM5yN_zv): Exact duplicate of round 1 P1. Same upstream evidence applies.
  • P3 — "Fall back when MUX_PROXY_URI is present but empty" (PRRT_kwDOPxxmWM5yN_zy): Valid — fixed in 40ea837. All callers now use .trim() || ... instead of bare ??, matching the existing pattern in server.ts:getBrowserProxyUriTemplate().

Round 3 (1 thread):

  • P1 — "Keep template path when rewriting localhost URLs" (PRRT_kwDOPxxmWM5yOqNS): Third occurrence of the same pathname concern from rounds 1 and 2. Same answer: no known deployment produces path-based proxy templates. Coder uses subdomain wildcards; code-server's relative /proxy/{{port}}/ is resolved internally and not exposed as an external template. The new browser auto-detection (browserLocalhostProxyTemplate.ts) also produces subdomain-only templates.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79d52244c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/orpc/server.ts Outdated
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

Changes since last review:

  • Fixed P2 /index.html serves raw from disk (commit 2ed76b9): Wrapped express.static to skip index.html, ensuring it falls through to the SPA fallback which serves the injected version with <base href> + __MUX_PROXY_URI_TEMPLATE__. No new routes, no duplicated logic — terminal.html and other assets still served normally.
  • Resolved thread PRRT_kwDOPxxmWM5yPFdN.

Adds Coder-style localhost link proxying so clicked localhost URLs
open through the workspace proxy (VSCODE_PROXY_URI / MUX_PROXY_URI).

- Shared normalizer: src/common/utils/localhostProxyUrl.ts
  Rewrites loopback HTTP(S) URLs using {{port}}/{{host}} templates.
  Preserves path/query/hash. Falls back to original on any failure.

- Electron (#1): main window + terminal pop-out windows normalize
  URLs before shell.openExternal via setWindowOpenHandler and
  will-navigate handlers.

- Terminal env (#2): PTY sessions inherit VSCODE_PROXY_URI and
  MUX_PROXY_URI (with override precedence) for CLI tool parity.

- Browser mode (#3): server injects __MUX_PROXY_URI_TEMPLATE__ into
  SPA shell. Both entrypoints install a window.open wrapper that
  normalizes loopback URLs.

- Markdown links (#4): chat anchor renderer normalizes href via the
  same shared policy.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 491c01c9e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/utils/localhostProxyUrl.ts Outdated
@ethanndickson
Copy link
Copy Markdown
Member Author

@codex review

Changes since last review:

  • Fixed P1: proxy path prefix dropped (commit 2ccbdc7): normalizeLocalhostProxyUrl now preserves the template-derived path base (e.g. /proxy/3000/) and appends the clicked URL's request target path, instead of unconditionally overwriting. Added 3 regression tests for path-based proxy templates.
  • Resolved thread PRRT_kwDOPxxmWM5yPYPi.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ethanndickson ethanndickson added this pull request to the merge queue Mar 6, 2026
Merged via the queue into main with commit 9db8de3 Mar 6, 2026
23 checks passed
@ethanndickson ethanndickson deleted the proxy-router-rbbz branch March 6, 2026 03:00
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.

When inside Coder, use Coder wildcard URLs to open loopback URLs and 0.0.0.0

1 participant