---
title: "tailscale serve 子路径导致 JS/CSS 404 怎么办"
description: "应用挂在 /app 下后 /assets、/@vite/client 和登录重定向为什么会失效，以及两条出路：让应用和上游就前缀达成一致，或给它独立名字并挂在 / 下。"
url: "https://tslink.md/zh/docs/serve-set-path-breaks-app"
locale: "zh"
product_version: "0.2.0"
source: "https://github.com/anydoor7/tslink/blob/v0.2.0/docs/architecture.md"
---

> Documentation index: https://tslink.md/zh/llms.txt · Installed binary is authoritative: `tslink manifest`.

**把应用挂在 `/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` 参考](https://tailscale.com/docs/reference/tailscale-cli/serve)）。执行之后，应用的地址就变成设备名加上 `/app`：

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

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

| 现象                                                 | 报告                                                                                                                                  |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `GET /@vite/client 404` 和 `ERR_TOO_MANY_REDIRECTS` | [Farplane-UI issue 24](https://github.com/ZanarkandTechnologies/Farplane-UI/issues/24)，路径前缀后面的 Vite 应用                              |
| JS、CSS、API 调用等资源加载失败                               | [HolyClaude issue 64](https://github.com/CoderLuii/HolyClaude/issues/64)，退回根路径挂载后绕过                                                 |
| 前端应用默认路径从 `/` 开始                                   | [tailscale issue 16389](https://github.com/tailscale/tailscale/issues/16389)                                                        |
| 页面只显示了一部分                                          | [tailscale issue 12413](https://github.com/tailscale/tailscale/issues/12413)                                                        |
| 登录后跳到不带 `/adguard` 前缀的 `/login.html`，把自己锁在外面       | [Reddit r/Tailscale，2023 年 5 月](https://www.reddit.com/r/Tailscale/comments/137ubdx/tailscale_serve_webapps_how_to_deal_with)       |
| 把 Web 应用放到路径下运行，本来就会这样                             | [Reddit r/Tailscale，2025 年 7 月](https://www.reddit.com/r/Tailscale/comments/1lyi5t3/tailscale_serve_path_routing_for_web_apps_like) |

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

## Serve 怎样处理前缀

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

## 方向一（未测试）：让应用和上游就前缀达成一致

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

* 只设置 `base: '/app/'`，而 Serve 又去掉 `/app`，是行不通的。Vite 开发服务器会把对 `/` 的请求重定向到 base，对 `/@vite/client` 这类不带前缀的路径返回 404（见 [Vite 8.3.5 的 base 中间件](https://github.com/vitejs/vite/blob/v8.3.5/packages/vite/src/node/server/middlewares/base.ts)）。上游必须收到前缀，例如把 Serve 的目标写成本机 3000 端口并带上 `/app/` 路径，开发服务器的 Host 白名单仍然生效（见 [Vite Blocked request 页面](https://tslink.md/zh/docs/vite-blocked-request-ts-net.md)）。
* Vite 文档里的 `base` 讲的是部署：在嵌套的公共路径下，`vite build` 的产物会相应改写资源路径（见 [Public Base Path](https://vite.dev/guide/build#public-base-path)）。这并不能说明开发服务器挂在 Serve 后面时的行为。

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

## 方向二：给应用独立名字，挂在 /

每个应用有自己的名字之后，就没有应用需要知道前缀。TSLink 把每个注册的应用作为独立的 tsnet 节点运行，应用位于 443 端口的 `/`。[一台机器多个服务](https://tslink.md/zh/docs/several-apps-one-machine.md)解释了这个模型，以及它与 Tailscale Services、Docker sidecar 的对比；[分享一个应用](https://tslink.md/zh/docs/share-one-app.md)列出了每个节点暴露的内容。

如果之前为旧的挂载设置过 `base` 或带路径前缀的 Serve 目标，请改回默认的 `/`。开发服务器还可能检查新地址的 Host 头，这种情况见 [Vite Blocked request 页面](https://tslink.md/zh/docs/vite-blocked-request-ts-net.md)。

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

## 何时不该用 TSLink

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

## 相关页面

* [一台机器多个服务，每个都有自己的 ts.net 名字](https://tslink.md/zh/docs/several-apps-one-machine.md)：模型、与 Services 的对比以及计费。
* [分享一个应用，而非整台机器](https://tslink.md/zh/docs/share-one-app.md)：每个应用的访问规则，以及各节点暴露的内容。
* [在手机上使用编程 agent 的 Web UI](https://tslink.md/zh/docs/agent-ui-phone.md)：开发服务器在手机上打开时同样适用的做法。
* [工具对比](https://tslink.md/zh/docs/comparisons.md)：TSLink、TSDproxy、tsnsrv 及其他工具。

以上 Tailscale 与 Vite 官方来源访问日期：**2026-10-22**。TSLink 行为以 v0.2.0 为准。TSLink 是 [anydoor7](https://github.com/anydoor7/tslink) 的独立项目，**并非由 Tailscale 制作或背书**。
