← Back to PRs

#7852: fix(gateway): use port from Host header for canvasHostUrl (reverse proxy support)

by tonimelisma open 2026-02-03 07:55 View on GitHub →
stale
## Summary Fixes a bug where mobile apps fail to load A2UI when connecting through a reverse proxy (e.g., Tailscale Serve 443 → 18789). **Root cause:** The gateway always used the internal `canvasPort` when constructing `canvasHostUrl`, ignoring the port from the HTTP Host header. **Fix:** Parse the port from the Host header and prefer it over the internal `canvasPort`. --- ## Problem When connecting via reverse proxy: | Host Header | canvasPort | Before (broken) | After (fixed) | |-------------|------------|-----------------|---------------| | `node.tailnet:443` | 18789 | `https://node.tailnet:18789` ❌ | `https://node.tailnet:443` ✅ | Mobile apps tried to load A2UI from port 18789, which isn't exposed through the reverse proxy → connection failed. --- ## Changes ### `src/infra/canvas-host-url.ts` 1. **Added `parseHostPort()`** - Extracts port from Host header (e.g., `"node.tailnet:443"` → `443`) 2. **Fixed `parseHostHeader()`** - Strip IPv6 brackets (Node 25+ returns them in `hostname`) 3. **Updated `resolveCanvasHostUrl()`** - Prefer Host header port, fall back to `canvasPort` ### `src/infra/canvas-host-url.test.ts` (new) 11 test cases covering: - Tailscale Serve regression case - Fallback to canvasPort when no port in Host header - Direct connections, IPv6, default ports (80/443) - Edge cases (invalid headers, missing values) --- ## Backwards Compatibility ✅ **No breaking changes** - If Host header has no port, falls back to existing `canvasPort` behavior. --- ## Testing ### Automated ``` ✓ src/infra/canvas-host-url.test.ts (11 tests) 3ms ``` ### Manual - **Environment:** Tailscale Serve (443 → 18789) - **Result:** A2UI loads successfully with correct port ``` [GatewayNode] handlePush(.snapshot) raw canvasHostUrl='https://ubuntu-4gb-hel1-1.tailed3476.ts.net:443' [ScreenController] didFinish url='https://ubuntu-4gb-hel1-1.tailed3476.ts.net/__openclaw__/a2ui/?platform=ios' ``` --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR updates `resolveCanvasHostUrl()` to prefer the explicit port from the incoming HTTP `Host` header (via a new `parseHostPort()` helper), falling back to the configured internal `canvasPort` when the header has no port. It also adjusts `parseHostHeader()` to strip IPv6 brackets and adds a new Vitest suite covering reverse-proxy, IPv6, default-port, and fallback scenarios. Overall this aligns the constructed A2UI base URL with what reverse proxies actually expose (e.g., Tailscale Serve 443 → internal port). <h3>Confidence Score: 4/5</h3> - This PR is largely safe to merge and fixes a real reverse-proxy correctness issue, with minor edge-case concerns around port selection/validation. - Core change is small, covered by targeted tests, and uses the URL parser to handle IPv4/IPv6/host:port forms. The main risks are behavioral surprises when `hostOverride` is set (hostname overridden but port still taken from `Host`) and lack of numeric range validation for parsed ports, which could yield invalid URLs in misconfigured environments. - src/infra/canvas-host-url.ts <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs