Symptom
Two agents independently verified frontend changes against http://localhost:5173. Both got HTTP 200 with a fully rendered page. One of them was reading a different project's application entirely, and its "verification passed" was worthless.
Cause
localhost resolves to both ::1 and 127.0.0.1, and most HTTP clients try IPv6 first. On this machine:
- The project's dev stack (running under OrbStack/Docker) bound IPv4 only:
127.0.0.1:5173. - An unrelated project's Vite dev server was listening on
[::1]:5173.
So curl http://localhost:5173/ hit the other app and returned 200 with real HTML. There is no error anywhere in the chain. The failure mode is a wrong-page read that is indistinguishable from success — worse than a connection refused, which at least announces itself.
It also silently breaks CORS-sensitive flows in the opposite direction: a backend whose allowlist is ^http://localhost:5173$ will reject an origin of http://127.0.0.1:5173, so "just use the IP" can trade a wrong-page read for a CORS failure.
Diagnostic
# What does localhost actually resolve to, and who is listening on each?
python3 -c "import socket;print(socket.getaddrinfo('localhost',5173))"
lsof -nP -iTCP:5173 -sTCP:LISTEN # shows IPv4 vs IPv6 per listener (note the ::1 rows)
curl -s -o /dev/null -w '%{remote_ip}\n' http://localhost:5173/The last one is the fastest tell: if remote_ip is ::1 and your stack is Docker-published on IPv4, you are talking to something else.
Procedure
- Address the dev stack by explicit IP (
127.0.0.1), neverlocalhost, when the stack is container-published — Docker/OrbStack port publishing is commonly IPv4-only. - Assert something app-specific in the body before believing a 200. A status code is not identity. Grep for a string only your app emits (a route-specific title, a known component's text). Any verification whose only evidence is
200is not a verification. - When a browser/CORS-sensitive origin is required, force resolution instead of changing the URL: Chrome accepts
--host-resolver-rules="MAP localhost 127.0.0.1", which keeps the origin literallyhttp://localhost:5173(so the CORS allowlist matches) while connecting to IPv4.
Adjacent trap found in the same session
Vite's SSR module graph can serve a module several edits stale and will not invalidate on touch or rewrite. The symptom is editing a file, reloading, and seeing old output — which reads as "my change is wrong" rather than "my change was not loaded". Restart the dev container rather than debugging phantom stale behaviour.
Generalization
Any verification step should be able to fail. "I fetched a URL and got 200" cannot distinguish my app works from some app works, so it is not a test — it is a coincidence detector. Bind the assertion to identity, not availability.