Vite 启动后 localhost:5173 报 404,IP 直连却正常?罪魁祸首在废纸篓里

1 阅读4分钟

🚀 省流助手(速通结论)

  • 现象pnpm dev 启动 Vite 后,浏览器访问 http://localhost:5173/ 返回 404,但访问 http://127.0.0.1:5173/ 一切正常
  • 根因:之前删掉的项目目录里有一个 Vite 进程没被 kill,成了孤儿进程,绑在 IPv6 ::1:5173 上;macOS 的 localhost 优先解析到 IPv6,请求被这个孤儿进程接管,返回 404
  • 解决lsof -i :5173 找出多余的 node PID,kill <PID> 即可

一、现象:奇怪的「半通不通」

某天打开新项目准备开发,习惯性 pnpm dev

$ pnpm --filter=@your-org/your-app run dev

> @your-org/your-app@0.0.0 dev
> vite --open

  VITE v5.4.21  ready in 1045 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: http://10.0.0.99:5173/

启动看上去一切正常。但浏览器自动打开 http://localhost:5173/ 之后:

找不到与以下网址对应的网页:http://localhost:5173/
HTTP ERROR 404

奇怪的是——把 URL 换成 Network 那一行的 IP(http://10.0.0.99:5173/),页面完全正常

第一反应:「应该是浏览器缓存吧?」

开个无痕窗口——还是 404。

「换 Chrome 试试?」——还是 404。

昨天还好好的,今天就这样了。

二、排查路径:层层排除

假设 1:浏览器层问题(缓存 / Service Worker / 扩展)

最常见的可能。但隐身窗口也 404,多个浏览器都 404 ——浏览器层基本可以排除。

假设 2:本地代理软件劫持

有些代理工具(Clash、Surge 之类)会顺手把 localhost 流量也挂到自己的端口上。临时关掉系统代理 + 关掉所有代理客户端,情况依旧——排除。

假设 3:hosts 文件被改

$ cat /etc/hosts | grep localhost
127.0.0.1 localhost
::1       localhost

干净。排除。

到这里如果还停留在猜测,会越走越偏。该让证据说话了。

三、关键证据:终端三连击

第一发:lsof 看谁占着 5173

$ lsof -i :5173
COMMAND   PID      USER   FD   TYPE   NODE NAME
node     4040 <user>     37u  IPv4   TCP  *:5173 (LISTEN)
node    20742 <user>     37u  IPv6   TCP  localhost:5173 (LISTEN)

两个 node 进程?! 一个绑在 IPv4 通配地址,一个绑在 IPv6 localhost

我刚启动的 Vite 应该只有一个,那 PID 20742 是哪来的?

第二发:curl 分别打三种地址

# IPv4 直连
$ curl -v http://127.0.0.1:5173/
< HTTP/1.1 200 OK
< Content-Type: text/html
(正常返回 vite 的 index.html)

# IPv6 直连
$ curl -v http://[::1]:5173/
< HTTP/1.1 404 Not Found
< Content-Length: 0

# localhost 让系统决定
$ curl -v http://localhost:5173/
*   Trying [::1]:5173...        ← 注意这里!localhost 解析到了 ::1
< HTTP/1.1 404 Not Found

谜底揭开一半:

  • PID 4040(我刚启的 Vite)在 IPv4 127.0.0.1:5173 上,正常返回 200
  • PID 20742(来路不明)在 IPv6 ::1:5173 上,返回 404
  • macOS 上 localhost 优先解析到 ::1,所以浏览器和 curl localhost 默认走 IPv6,被另一个进程接管

第三发:PID 20742 到底是谁?

$ ps -p 20742 -o pid,ppid,user,command
  PID  PPID USER     COMMAND
20742 20729 <user>   node /Users/<user>/projects/legacy-web/node_modules/...

$ lsof -p 20742 | grep cwd
node 20742 <user> cwd  DIR  /Users/<user>/.Trash/legacy-web

看到 cwd(current working directory)那行了吗?

这个进程的工作目录在 .Trash 里。

也就是说,我之前把一个叫 legacy-web 的旧项目目录拖进了废纸篓——但当时它的 pnpm dev 还在跑。进程没被 kill 掉,孤零零地继续监听 5173 端口。

新项目今天要用 5173,Vite 拿不到 IPv6(被孤儿占了),只绑成功了 IPv4。于是浏览器走 localhost 默认 IPv6 时,永远撞上孤儿进程,返回 404。

四、根因

三件事叠加,触发了这个诡异现象:

  1. Vite dev server 在删除项目时不会自动停止——文件系统的 unlink 不会通知到进程,进程的 cwd 指向已删除目录,但内核保留了 inode 引用,进程照常运行
  2. Vite 默认在 IPv6 :: 上 listen——Node 的 server.listen('localhost') 在双栈系统上会同时绑 IPv4 和 IPv6,所以你看到的 *:5173localhost:5173 实际上是两个 socket
  3. macOS 的 getaddrinfo 默认优先返回 IPv6——浏览器拿 localhost 去解析时,先拿到 ::1,于是走 IPv6 路径

三者叠加 → localhost 被孤儿 Vite 截胡。

五、解决方案

临时救火

# 找到孤儿进程
$ lsof -i :5173

# 干掉它
$ kill 20742

立刻恢复正常。

永久方案:Vite 强制只绑 IPv4

如果你想避免类似问题,编辑 vite.config.ts

export default defineConfig({
  server: {
    host: '127.0.0.1', // 强制 IPv4
  },
});

或者养成习惯,开发时直接用 127.0.0.1 替代 localhost

六、预防建议

  • 删项目目录前先 Ctrl+C 停掉 dev server,或关闭对应终端,让进程优雅退出

  • 遇到「奇怪的 localhost 问题」,第一反应 lsof -i :端口 看占用情况,用证据排除假设比脑补更快

  • 调试网络问题时,curl 三连击是必备:

    • curl http://127.0.0.1:port(IPv4)
    • curl http://[::1]:port(IPv6)
    • curl http://localhost:port(让系统决定)

    对比三者的差异,能快速定位 IPv4/IPv6 双栈问题。

七、知识点提炼

7.1 localhost 到底解析到哪?

在 macOS / 多数现代 Linux 上,/etc/hosts 同时定义:

127.0.0.1 localhost
::1       localhost

getaddrinfo() 返回时,默认优先返回 IPv6 地址。所以 Node.js、curl、浏览器拿到的第一个地址通常是 ::1

如果想强制 IPv4:

  • Node 启动加 --dns-result-order=ipv4first
  • 直接用 127.0.0.1 替代 localhost

7.2 Vite 的 listen 行为

Vite 5.x 默认 server.hostlocalhost。底层 Node 在双栈系统上会同时绑 IPv4 和 IPv6——这就是 lsof 里能看到一个 IPv4 socket 加一个 IPv6 socket 的原因。

如果其中一个端口被占用,Vite 不会自动换端口,会启动失败;但如果 IPv4 没被占、只有 IPv6 被占(像本文的情况),Vite 会只成功绑 IPv4,看起来一切正常,但默认路径上的请求会被孤儿进程截胡。

7.3 为什么孤儿进程不会自动退出?

Unix 进程的工作目录被删除后:

  • 内核保留 inode——只要还有进程持有引用,目录在文件系统视图上消失了,但实际内容还在
  • 进程不会主动收到信号——除非父进程 SIGTERM 它,或它自己注意到资源消失
  • Vite 的 file watcher(fsevents)在目录被删时可能会报错,但不会主动退出 dev server

类似的孤儿进程模式还会在以下场景出现:

  • VSCode 关闭但终端中的 dev server 没退出
  • macOS 强制注销而某些 GUI 应用未正常关闭
  • 容器被 docker stop 但容器内进程对 SIGTERM 没响应

养成 lsof -ips aux | grep node 的肌肉记忆,能省下很多 debug 时间。


写于一次愉快的 debug 之后。如果你也遇到了「localhost 神秘 404」,希望这篇能帮你少走 30 分钟弯路 🚀