Symptom
Two failures in sequence while driving a SvelteKit dev stack (Docker/OrbStack, frontend 5173, API 8081) through headless/headed Chrome:
http://localhost:5173/served a completely different project's app.document.titlewas another product's name; the URL bar still saidlocalhost:5173/<my-route>.- Switching to
http://127.0.0.1:5173/loaded the right app, but the authenticated chat feature hung forever on a "Setting up..." spinner. No console error, no failed request in the log; the server-side logs showed the API endpoints returning 200.
Cause
$ lsof -nP -iTCP:5173 -sTCP:LISTEN
node 43504 me 31u IPv6 ... TCP [::1]:5173 (LISTEN)
OrbStack 97405 me 216u IPv4 ... TCP *:5173 (LISTEN)
OrbStack 97405 me 305u IPv6 ... TCP *:5173 (LISTEN)A stray Vite dev server from an unrelated checkout was bound to the specific address [::1]:5173. Docker held the wildcard *:5173. Both binds succeed — they are different addresses, so there is no "port already in use" error anywhere. macOS resolves localhost to ::1 first, and a specific bind beats a wildcard bind, so every localhost:5173 request went to the wrong app while 127.0.0.1:5173 went to the right one.
The second failure is the workaround's fault. Session cookies were set by the frontend origin without a Domain attribute, making them host-only. 127.0.0.1 and localhost are different hosts. With the page served from 127.0.0.1:5173 and the client calling the API at localhost:8081 (baked in at build time via VITE_*), no cookie was attached to client-side API calls. The feature's SSE stream authenticated as anonymous and never produced output, which surfaced as an indefinite loading state rather than a 401.
Fix
Don't change the origin. Change the browser's resolution of it:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9333 \
--user-data-dir=/tmp/chrome-scratch \
--host-resolver-rules="MAP localhost 127.0.0.1"The page origin stays localhost:5173 (so host-only cookies match the API's localhost:8081) while resolution is pinned to IPv4, which is the container. --host-resolver-rules is process-scoped, so it doesn't touch /etc/hosts or the user's normal browser. Then attach over CDP.
Checks to run first
Cheap, and they would have saved two dead ends:
lsof -nP -iTCP:$PORT -sTCP:LISTEN # who really owns it, per-address
curl -s localhost:$PORT | grep -o '<title>[^<]*' # is this even my app?
curl -s 127.0.0.1:$PORT | grep -o '<title>[^<]*' # do v4 and v6 differ?If the two curls disagree, you have a split bind. docker compose ps showing 0.0.0.0:5173->5173/tcp and the container running is not evidence that localhost:5173 reaches it.
Generalization
- An indefinite loading spinner on an authenticated surface, with 200s in the server log, is the signature of a request going out without credentials — check origin/host equality before you check the feature.
localhostis not a synonym for127.0.0.1on any dual-stack machine, and specific binds shadow wildcard binds without any collision error. Any agent that hardcodeslocalhost:<port>for browser automation is one stray dev server away from testing someone else's application and reporting confident findings about it.