PAYATHON 2026

NestJS Swagger UI 在 Android 真机上加载正常,为何请求报 CORS 错误?

排障赵师傅

先检查 Swagger 实际请求的地址

先看 Swagger 的 servers 配置:

.addServer('http://localhost:3000', 'Local Development')
.addServer('http://192.168.1.5:3000', 'Local Network')

如果 Swagger UI 当前选中的是 http://localhost:3000,手机就会向自身的 3000 端口发送请求。手机上的 localhost 指向手机,不会指向 MacBook,因此修改 MacBook 上的 CORS 配置也无法解决这个地址错误。

这次测试可以让文档页面和 API 都使用 http://192.168.1.5:3000。两者同源时,请求不需要跨域授权。报错中的 Possible reasons: CORS, Network failure... 只是通用提示,单凭这段信息无法确定问题出在 CORS。

为什么文档能打开,API 请求却失败?

加载 Swagger 页面和点击 Try it out 时,浏览器使用的地址可能不同:

  • 文档页面使用浏览器地址栏中的 URL。
  • API 请求使用 Swagger UI 当前选择的 server 地址,再拼接接口路径。

打开:

http://192.168.1.5:3000/api/docs

并不代表 /auth/login 一定会请求:

http://192.168.1.5:3000/auth/login

它也可能请求:

http://localhost:3000/auth/login

文档能打开,说明手机能够访问 MacBook 的这个地址和端口。Swagger 生成的 API 请求是否使用了相同地址,还需要单独确认。

协议、主机和端口都相同才算同源,路径不参与判断:

页面地址API 地址是否同源
http://192.168.1.5:3000/api/docshttp://192.168.1.5:3000/auth/login是
http://192.168.1.5:3000/api/docshttp://localhost:3000/auth/login否,且目标是手机自身
http://192.168.1.5:5175http://192.168.1.5:3000/auth/login否,端口不同

Android Emulator 中的 10.0.2.2 是访问宿主机回环接口的特殊地址。Android 真机没有这层映射,应使用 MacBook 的局域网 IP。

第一步:修正 Swagger 的 server 地址

在 Swagger UI 的 Servers 下拉框中选择:

http://192.168.1.5:3000

再查看请求区域的 Request URL,确认请求指向 MacBook。

如果文档和 API 始终使用同一个地址,可以使用相对 server URL:

const swaggerConfig = new DocumentBuilder()
  .setTitle('Liaison360 API')
  .addServer('/', 'Current origin')
  .build();

这样,用手机访问局域网地址时,API 请求会使用该地址;在 MacBook 上通过 localhost 访问时,请求也会跟随当前页面的来源。

这里假设 API 位于站点根路径下,例如 /auth/login。如果配置了全局前缀或反向代理路径,需要检查生成的 OpenAPI paths 和最终的 Request URL,避免路径前缀重复或缺失。

也可以只保留局域网地址,但需要保证 MacBook 的 IP 不变:

const swaggerConfig = new DocumentBuilder()
  .setTitle('Liaison360 API')
  .addServer('http://192.168.1.5:3000', 'Local Network')
  .build();

第二步:按实际页面来源配置 CORS

origin 白名单应填写发起请求的页面来源,而不是 API 的目标地址。

当前配置:

origin: ['http://localhost:5175']

允许通过 http://localhost:5175 打开的 Vite 页面跨域访问后端。如果手机通过 http://192.168.1.5:5175 打开 Vite 页面,就需要把这个来源也加入白名单。

可以使用下面的配置,支持电脑和手机上的 Vite 页面,同时让 Swagger 请求跟随当前页面的来源:

import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableCors({
    origin: [
      'http://localhost:5175',
      'http://192.168.1.5:5175',
    ],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
  });

  const swaggerConfig = new DocumentBuilder()
    .setTitle('Liaison360 API')
    .addServer('/', 'Current origin')
    .build();

  const document = SwaggerModule.createDocument(app, swaggerConfig);
  SwaggerModule.setup('api/docs', app, document);

  await app.listen(3000, '0.0.0.0');
}

bootstrap();

只有通过 http://192.168.1.5:5175 访问 Vite 时,才需要允许这个来源。Vite 开发服务器本身也必须允许局域网访问。

如果 Swagger 页面和 API 都在 http://192.168.1.5:3000,就不必为了 Swagger 把这个来源加入白名单。如果 Swagger 需要跨源调用另一个 API,应在目标 API 的 CORS 配置中允许 Swagger 页面的来源,例如:

origin: [
  'http://localhost:5175',
  'http://192.168.1.5:5175',
  'http://192.168.1.5:3000',
]

来源只包含协议、主机和端口,不要带上路径,例如 http://192.168.1.5:3000/api/docs。

修改后如何确认问题所在?

重启后端并刷新 Swagger 页面,按以下顺序检查:

  1. 查看 Servers 和 Request URL,确认请求指向 192.168.1.5:3000。
  2. 使用 Android Chrome 远程调试,查看 Network 和 Console 中的具体错误。
  3. 如果有跨域 OPTIONS 预检请求,检查响应中的 Access-Control-Allow-Origin、Access-Control-Allow-Methods 和 Access-Control-Allow-Headers。
  4. 如果请求发生重定向,检查重定向后的地址,尤其要留意是否跳到了 localhost、其他端口或不同协议。

跨域发送 Content-Type: application/json 或 Authorization 时,通常会触发预检。需要确保预检请求能够通过,前置代理或鉴权逻辑不能将其拦截。

如果最终请求始终与文档同源,就继续排查连接失败、重定向,以及浏览器控制台报告的其他限制。普通的 401、404、500 响应本身通常不会让 fetch 以网络错误的形式失败。

credentials: true 的注意事项

credentials: true 表示服务端允许携带凭据的跨域请求。它无法修复请求地址错误,也不会自动让客户端发送 Cookie。

如果使用跨域 Cookie:

  • Access-Control-Allow-Origin 必须返回明确的来源,不能使用 *。
  • 客户端也需要启用凭据发送;Swagger UI 可按需配置 swaggerOptions: { withCredentials: true }。
  • Cookie 是否发送还受 SameSite、Secure 等属性影响。

如果只是通过 Authorization: Bearer ... 传递令牌,通常不需要为此开启 credentials: true,但跨域预检仍需允许 Authorization 请求头。

备注:内容仅供参考。