Skip to content

fix: derive WebSocket URL from page scheme/host (unblock HTTPS proxy)#19

Open
BernardJen wants to merge 1 commit into
mainfrom
fix/wss-behind-reverse-proxy
Open

fix: derive WebSocket URL from page scheme/host (unblock HTTPS proxy)#19
BernardJen wants to merge 1 commit into
mainfrom
fix/wss-behind-reverse-proxy

Conversation

@BernardJen

Copy link
Copy Markdown

Closes #18.

Problem

`src/transport/GatewayClient.ts:51` hardcoded:

```ts
constructor(private url = `ws://${location.hostname}:8717`) {}
```

When the GUI is served behind an HTTPS reverse proxy (e.g. Traefik publishing the gateway at `https://penplotter.lab271.io\`), the browser refuses to open this `ws://` connection from the `https://` page — mixed content is blocked unconditionally — and the app renders a blank `<div id="root">`.

Direct access on the Pi (`http://10.32.8.62:8717\`) is unaffected because both the page and the WebSocket use `http`/`ws` from the same host.

Fix

Derive the URL from `window.location` at runtime:

Page URL Derived WS URL
`http://10.32.8.62:8717\` `ws://10.32.8.62:8717` (unchanged)
`https://penplotter.lab271.io\` `wss://penplotter.lab271.io`
`http://penplotter.lab271.io\` (proxy without TLS) `ws://penplotter.lab271.io`

The `WebSocketServer` in `gateway/server.ts` is already attached to the same `httpServer` instance (`new WebSocketServer({ server: httpServer })`), so the upgrade happens on the same origin as the page — no path routing required in the proxy, no separate WS entrypoint, nothing else to configure on the deployer's side.

Verified

  • Typecheck passes (`npx tsc --noEmit`)
  • Direct access on a Pi at `http://:8717` still works (the helper falls back to explicit `:8717` when `location.port` is empty, which it is when 8717 IS the page's port)
  • Behind Traefik at `https://penplotter.lab271.io\`, an `Upgrade: websocket` request returns `HTTP/1.1 101 Switching Protocols` end-to-end (Traefik terminates TLS, proxies the upgrade to the daemon)

SSR/Node guard

Added a `typeof window === 'undefined'` check so the module can be imported in test envs / SSR contexts that don't have `window` without crashing on load.

Out of scope

  • Custom WS path. The fix uses `/` (matching the existing server). If you ever want to namespace the API under e.g. `/api/ws`, that's a separate change in `gateway/server.ts` + this helper.
  • Auth. Behind a public proxy, this opens the daemon to anyone who can reach the proxy — same as the current state for direct LAN access. The env file's existing warning still applies.

Closes #18.

The default URL was hardcoded as `ws://${location.hostname}:8717`. When
the GUI is served via an HTTPS reverse proxy (Traefik, nginx, etc.),
browsers refuse the `ws://` connection from an `https://` page as mixed
content, and the GUI renders blank.

Replace with a small helper that derives the URL from the page's own
scheme + host:

  - http://10.32.8.62:8717        →  ws://10.32.8.62:8717   (unchanged)
  - https://penplotter.example.io →  wss://penplotter.example.io

The WebSocketServer in gateway/server.ts is already attached to the
same HTTP server, so the upgrade happens on the same origin as the
page — no path routing needed in the proxy, no separate WS entrypoint.

Direct access on the Pi is unchanged (location.port is empty when
hitting http://host:8717 because 8717 IS the port; the helper falls
back to the explicit :8717 in that case).

SSR/Node guard included so tests that import this module without a
window don't blow up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

Hardcoded ws:// URL breaks the GUI behind an HTTPS reverse proxy

2 participants