Skip to content

[Fix] HMR WebSockets rejected in sandbox previews - #1217

Open
mrubens wants to merge 1 commit into
developfrom
fix/vite-preview-hmr-allowed-hosts
Open

[Fix] HMR WebSockets rejected in sandbox previews#1217
mrubens wants to merge 1 commit into
developfrom
fix/vite-preview-hmr-allowed-hosts

Conversation

@mrubens

@mrubens mrubens commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem

HMR is broken in sandbox previews for any Vite-based app (Astro, Nuxt, SvelteKit included). The page renders and every HTTP request returns 200, but the HMR WebSocket never connects, so live reload silently does nothing.

Vite gates both requests and HMR upgrades on server.allowedHosts. Two different hosts reach a sandbox dev server:

  • Plain HTTP arrives with the sandbox's own host, because preview-proxy creates its proxy with changeOrigin: true.
  • WebSocket upgrades arrive with the public preview host, because the auth-proxy rewrites Host on the upgrade path (startMultiplexAuthProxy).

Only the first was ever allowlisted, and only because individual repos hardcoded a preview domain in their own config. The upgrade is therefore rejected with a 400 — which is why previews render but HMR does not work.

Fix

Set __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS in the worker-managed shell env file (~/.roomote/env.sh, sourced via BASH_ENV and .bashrc) to both hosts. Vite reads the variable when it resolves config and appends it to allowedHosts, so HMR works with no per-repo configuration.

The value is pinned to the current task's exact hostnames rather than a wildcard suffix, deduplicated across named ports, and reuses the preview URLs injectEnvVars already builds:

__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=task-123-web.preview.example.run,sandbox-web.<provider-host>

The code-server port is excluded (it is not a user dev server), the nested-preview subdomain suffix is carried through, and an explicit deployment-provided value still wins — matching how PREVIEW_DOMAINS behaves directly below it.

Trade-off worth knowing

Exact hostnames mean a port registered after the env file is written is not covered until the file is refreshed and the dev server restarts (Vite reads the variable at startup). Nested previews are covered only for the suffix the task was spawned with. Switching to .${previewDomain} is a one-line change if either becomes a problem.

Testing

  • pnpm vitest run src/commands/__tests__/utils.test.ts in apps/worker — 38 pass, 7 new: exact preview hostnames, sandbox machine host, dedup across ports, nested subdomain suffix, code-server exclusion, deployment override, and no-base-URL.
  • pnpm check-types, pnpm lint, pnpm format:check clean; pre-push checks pass.

Follow-up in another repo

Once this ships, repos no longer need their own preview-domain allowlists. The website's astro.config.mjs has a stale one ready to delete — but it must land after this rolls out to the sandbox image, since one of its entries is currently what makes HTTP previews work.

Vite refuses requests and HMR WebSocket upgrades whose Host header is not
in `server.allowedHosts`, and Astro, Nuxt and SvelteKit inherit that check.
Two different hosts reach a sandbox dev server: the sandbox's own host on
plain HTTP, because the preview proxy changes origin, and the public preview
host on upgrades, because the auth-proxy rewrites Host there. Only the first
was ever allowlisted — by each repo hardcoding a preview domain in its own
config — so previews rendered while HMR died with a 400 and live reload
silently stopped working.

Set `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the worker-managed shell env
file to both hosts, as exact hostnames for the current task rather than a
wildcard suffix. Vite reads the variable when it resolves config and appends
it to allowedHosts, so HMR works without per-repo configuration. An explicit
deployment-provided value still wins.
@roomote-community

roomote-community Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

1 issue outstanding. See task

  • apps/worker/src/commands/utils/env-vars.ts:270 Vite appends __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS as one host rather than parsing a comma-separated list, so neither generated hostname is allowed.

Reviewed b461bbf

].filter((hostname) => !VITE_UNSAFE_ALLOWED_HOST_CHARS.test(hostname));

if (allowedHosts.length > 0) {
envVars.__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS = allowedHosts.join(',');

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.

Vite treats __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS as one host, not a comma-separated list: its resolver appends the entire environment value as a single allowedHosts entry. With the normal preview and sandbox hostnames, this produces task-123-web.preview...,sandbox-web... as one value, which matches neither Host header, so the HMR upgrade remains rejected. The new tests only split the value themselves and therefore do not exercise Vite's parsing behavior.

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