Skip to content

fix(dev): make Vite dev server reachable from LAN browsers#764

Merged
ajslater merged 1 commit into
developfrom
claude/sad-hawking-1449d6
May 12, 2026
Merged

fix(dev): make Vite dev server reachable from LAN browsers#764
ajslater merged 1 commit into
developfrom
claude/sad-hawking-1449d6

Conversation

@ajslater
Copy link
Copy Markdown
Owner

Summary

Three coupled issues prevented browsing the dev server from a second
Mac on the LAN (with the host advertised via mDNS as e.g. hooloovoo.local).
None reproduce on the host itself, which made the symptoms misleading.

bin/dev-server.sh

Dropped export VITE_HOST=localhost. The line overrode the auto-derivation
in codex/settings/__init__.py:_vite_dev_server_host(), so django-vite
rendered <script src="http://localhost:5173/..."> regardless of mDNS,
and any browser that wasn't on the host Mac tried to load the bundle
from its own loopback.

frontend/vite.config.js

  • server.cors.origin — Vite 6+ defaults server.cors.origin to a
    regex that only matches localhost / 127.0.0.1 / [::1]. Browsers
    hitting Django at http://<host>:9810 and then fetching from
    http://<host>:5173 got Vary: Origin back but no
    Access-Control-Allow-Origin, so the scripts were blocked. Mirror
    allowedHosts into a port-agnostic CORS regex.
  • server.hmr.host — With server.host: true and no explicit
    hmr.host, Vite bakes localhost into the HMR client's
    directSocketHost and socketHost strings. Remote browsers opened
    the HMR WebSocket against their own loopback (ERR_CONNECTION_REFUSED).
    Set hmr.host to the same VITE_HOST / mDNS-mangled hostname Django
    uses for <script src> so the embedded URL agrees with where the page
    actually loaded the module from.
  • allowedHosts — Include the resolved HMR_HOST so an explicit
    VITE_HOST=... override (one that doesn't match os.hostname()) is
    also trusted by Vite's host-header check and the new CORS regex.

Vite still hardcodes localhost into one more string — the diagnostic
serverHost variable in @vite/client — but that's only used inside a
console error message template when HMR fails, never for an actual fetch.
Influencing it would require pinning server.host to a single name,
which loses loopback access from the host machine.

Test plan

  • On the host machine: ./bin/dev-server.sh and bun run dev (in
    frontend/) start cleanly; browsing http://<host>.local:9810/
    loads the app with HMR working.
  • On a second Mac on the LAN: browsing the same URL loads the app,
    script tags resolve, HMR WebSocket connects.
  • curl -s http://<host>.local:5173/static/@vite/client | grep -E 'serverHost|directSocketHost'
    shows directSocketHost = <host>.local:5173/... (only serverHost
    should still be localhost, which is cosmetic per above).
  • curl -sI -H "Origin: http://<host>.local:9810" http://<host>.local:5173/static/@vite/client
    returns an Access-Control-Allow-Origin header.
  • Localhost workflow on the host (http://localhost:9810/) still
    works.

🤖 Generated with Claude Code

Three coupled issues prevented browsing the dev server from a second
machine on the LAN.

bin/dev-server.sh
- Drop ``export VITE_HOST=localhost`` default. It overrode Django's
  ``_vite_dev_server_host()`` auto-derivation, so even with mDNS
  resolution working, django-vite rendered ``<script
  src="http://localhost:5173/...">`` and LAN browsers tried to load
  the bundle from their own loopback.

frontend/vite.config.js
- ``server.cors.origin``: Vite 6+ defaults to a regex matching only
  ``localhost`` / loopback origins. Cross-origin fetches from the
  Django dev server (``http://<host>:9810``) to the Vite dev server
  (``http://<host>:5173``) returned ``Vary: Origin`` without
  ``Access-Control-Allow-Origin`` and the browser blocked the
  scripts. Mirror ``allowedHosts`` into a port-agnostic CORS regex.
- ``server.hmr.host``: with ``server.host: true``, Vite bakes
  ``localhost`` into the HMR client's ``directSocketHost`` /
  ``socketHost`` strings. LAN browsers then opened the HMR
  WebSocket against their own loopback and got
  ERR_CONNECTION_REFUSED. Set ``hmr.host`` to the same
  ``VITE_HOST`` / mDNS-mangled hostname Django uses for the
  ``<script src>`` so the embedded URL agrees with where the page
  actually loaded the module from.
- Include the resolved host in ``allowedHosts`` so an explicit
  ``VITE_HOST=...`` override (not matching ``os.hostname()``) is
  also trusted by Vite's host-header check and the new CORS regex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ajslater ajslater merged commit 61f14ed into develop May 12, 2026
3 checks passed
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