TSLinkTSLink 文档

tailscale serve 子路径导致 JS/CSS 404 怎么办

应用挂在 /app 下后 /assets、/@vite/client 和登录重定向为什么会失效,以及两条出路:让应用和上游就前缀达成一致,或给它独立名字并挂在 / 下。

最后更新于

查看 Markdown

把应用挂在 /app 下会失效,原因是许多 Web 应用写出的是以根开头的 URL,例如 /assets/index.js、/@vite/client 或 /login。Serve 会在转发前去掉 /app,浏览器又从主机根路径请求这些地址,落在你的挂载点之外,于是返回 404,或者跳转到别处。 出路有两条:让应用和上游就前缀达成一致(未测试的方向,见下文);或者给应用一个独立名字,挂在 /。第二条做法如下,假设应用在 3000 端口:

bash
tslink add myapp --proxy localhost:3000 --allow you@example.com
tslink url myapp --wait

把登录名换成你的 Tailscale 账号。地址形如 <name>.<tailnet>.ts.net,应用位于 443 端口的 /。之后用 tslink remove myapp 移除。TSLink 是独立项目,并非由 Tailscale 出品。

哪里坏了,会看到什么

挂载来自类似下面的命令,其中 --set-path 把服务挂在某个 URL 路径下(见 tailscale serve 参考)。执行之后,应用的地址就变成设备名加上 /app:

bash
tailscale serve --bg --set-path /app 3000

大家遇到的故障集中在下面几种,每一条都有公开的报告,下表给出现象和出处:

现象报告
GET /@vite/client 404 和 ERR_TOO_MANY_REDIRECTSFarplane-UI issue 24,路径前缀后面的 Vite 应用
JS、CSS、API 调用等资源加载失败HolyClaude issue 64,退回根路径挂载后绕过
前端应用默认路径从 / 开始tailscale issue 16389
页面只显示了一部分tailscale issue 12413
登录后跳到不带 /adguard 前缀的 /login.html,把自己锁在外面Reddit r/Tailscale,2023 年 5 月
把 Web 应用放到路径下运行,本来就会这样Reddit r/Tailscale,2025 年 7 月

这些都是用户报告,并非 Tailscale 对受影响应用范围的说明。它们的共同点是:应用在编写时就假定自己位于主机根路径。

Serve 怎样处理前缀

Serve 在转发请求之前会去掉挂载前缀。Tailscale v1.104.1 的源码里,serve.go 用 http.StripPrefix 去掉前缀,注释写的是转发前先把挂载点从 URL 路径中去掉。所以 /app/assets/index.js 到达应用时是 /assets/index.js,应用并不知道 /app 的存在,写出的仍是以根开头的链接,落在挂载点之外。你给 Serve 的上游 URL 会与剩余路径拼接(同一文件的 976 到 988 行),命令行也接受包含路径的完整 URL,路径会出现在转发给上游的请求里。

方向一(未测试):让应用和上游就前缀达成一致

想保留同一个名字、用多个路径,就要让应用生成带前缀的链接,同时上游 URL 要把前缀带回给应用,因为 Serve 会把它去掉。多数框架都有 base path 设置,Vite 里是 base。有两点需要注意,我们都没有端到端测试过:

  • 只设置 base: '/app/',而 Serve 又去掉 /app,是行不通的。Vite 开发服务器会把对 / 的请求重定向到 base,对 /@vite/client 这类不带前缀的路径返回 404(见 Vite 8.3.5 的 base 中间件)。上游必须收到前缀,例如把 Serve 的目标写成本机 3000 端口并带上 /app/ 路径,开发服务器的 Host 白名单仍然生效(见 Vite Blocked request 页面)。
  • Vite 文档里的 base 讲的是部署:在嵌套的公共路径下,vite build 的产物会相应改写资源路径(见 Public Base Path)。这并不能说明开发服务器挂在 Serve 后面时的行为。

请把这当作可以尝试的方向,而不是验证过的配方。它也只对提供此类设置的应用有效,服务端生成的重定向或链接仍可能忽略它。表中 2023 年的 Reddit 帖子描述了没有这类设置时的另一种办法:在前面加一层改写响应的反向代理,发帖人说这种办法并不通用。

方向二:给应用独立名字,挂在 /

每个应用有自己的名字之后,就没有应用需要知道前缀。TSLink 把每个注册的应用作为独立的 tsnet 节点运行,应用位于 443 端口的 /。一台机器多个服务解释了这个模型,以及它与 Tailscale Services、Docker sidecar 的对比;分享一个应用列出了每个节点暴露的内容。

如果之前为旧的挂载设置过 base 或带路径前缀的 Serve 目标,请改回默认的 /。开发服务器还可能检查新地址的 Host 头,这种情况见 Vite Blocked request 页面。

把一个源拆成多个名字,有两点会变化。不同名字就是不同的源,这就是上面说的跨源读取的来由。如果浏览器 JavaScript 要读取另一个应用主机名上的 API 响应,就要为前端所在的源配置该 API 的 CORS 策略,或者让这些请求经过同源的反向代理。这是浏览器 CORS 协议带来的架构后果,并非 TSLink 的测试结果。eido issue 178 是一个有意把单个 ts.net 源按路径拆分的项目例子。此外,注册是按应用进行的,添加、取地址和移除都要对每个名字分别执行。

  • 应用提供了 base path 设置,而且你想保持同一个源。 前端和它的 API 必须同源时,保留路径路由并按方向一设置 base path,同样请先自己验证。
  • 你只有一个应用。 直接执行 tailscale serve 3000,它就挂在设备名的根路径下。参见何时使用 TSLink。
  • 希望由 Tailscale 统一管理跨主机的名字。 这正是 Tailscale Services 的用途,它需要带 tag 的主机和管理员权限。
  • 需要应用使用你自己的域名。 TSLink 只提供 *.ts.net 名字。

相关页面

以上 Tailscale 与 Vite 官方来源访问日期:2026-10-22。TSLink 行为以 v0.2.0 为准。TSLink 是 anydoor7 的独立项目,并非由 Tailscale 制作或背书。

目录