PAYATHON 2026

Axios 收到 4xx 响应时为何只有 Network Error?

支付阿杰

结论

402 Payment Required 不会直接导致 Axios 报 ERR_NETWORK。只要浏览器允许 JavaScript 读取响应,Axios 就会正常进入 catch,并通过 error.response.status 提供 402 状态码。

当前错误对象中只有 request,没有 response,说明浏览器在响应到达 Axios 之前将其拦截了。由于前后端使用不同端口,首先应检查 CORS 配置,特别是错误响应是否缺少 Access-Control-Allow-Origin。请求带有 Authorization 时还会触发预检,也要确认 OPTIONS 请求能够通过。

如果 CORS 没有问题,再排查 HTTPS 开发证书、代理连接和浏览器安全策略。OpenAPI Generator 7.9.0 通常不是问题所在,修改 validateStatus 也解决不了 ERR_NETWORK。

为什么服务器返回了 402,Axios 却看不到

浏览器开发者工具能看到服务器返回 402,不等于页面中的 JavaScript 有权读取该响应。

例如,服务器返回:

HTTP/1.1 402 Payment Required
Content-Type: application/problem+json

但没有返回:

Access-Control-Allow-Origin: http://localhost:5173

浏览器会将其判定为 CORS 失败,不会把状态码和响应正文交给 Axios。Axios 只能收到 XMLHttpRequest.onerror,最后表现为:

AxiosError: Network Error
code: ERR_NETWORK

请求中包含 Authorization,浏览器通常会先发送预检请求:

OPTIONS /api/v1/Dashboard/GetDashboardEntries

如果认证或授权中间件拒绝了这个请求,或者服务器没有允许 Authorization 请求头,真正的 POST 请求可能根本不会发出。

正确配置 ASP.NET Core CORS

开发环境可以只允许 Vite 页面实际使用的源地址:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
    {
        policy
            .WithOrigins("http://localhost:5173")
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseRouting();

app.UseCors("Frontend");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

中间件顺序不能放错。UseCors 应位于认证、授权和控制器端点之前:

app.UseRouting();
app.UseCors("Frontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

采用这个顺序后,即使认证或授权失败,或者控制器返回 402,响应仍然可以带上 CORS 头。

WithOrigins 中填写的是页面的 origin,不是 API 地址。当前场景应填写:

http://localhost:5173

下面三个地址属于不同的 origin,不能互换:

http://localhost:5173
https://localhost:5173
http://127.0.0.1:5173

如果认证依赖跨域 Cookie,服务器还需要允许凭据:

builder.Services.AddCors(options =>
{
    options.AddPolicy("Frontend", policy =>
    {
        policy
            .WithOrigins("http://localhost:5173")
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials();
    });
});

Axios 也需要启用凭据:

const axiosInstance = axios.create({
  withCredentials: true,
});

使用 AllowCredentials() 时,不能同时使用 AllowAnyOrigin()。如果只是通过 Authorization: Bearer ... 发送令牌,通常不需要 withCredentials 或 AllowCredentials()。

确保错误响应也经过 CORS 中间件

可以这样返回 402:

return StatusCode(
    StatusCodes.Status402PaymentRequired,
    new ProblemDetails
    {
        Status = StatusCodes.Status402PaymentRequired,
        Title = "Payment required",
        Detail = "An active subscription is required."
    });

CORS 不能只配置在成功执行的控制器路径上。如果自定义中间件、反向代理或认证处理程序在 UseCors 之前直接返回响应,401、402、403、500 等错误响应仍可能缺少 CORS 头。

同时要确认认证逻辑没有拦截预检请求。浏览器发送的 OPTIONS 请求通常不带 Bearer token,应由 CORS 中间件处理,不应先要求它通过业务授权。

Axios 中读取状态码

修复 CORS 后,可以直接检查 error.response:

import axios from "axios";

try {
  await dashboardApi.getDashboardEntries();
} catch (error: unknown) {
  if (axios.isAxiosError(error)) {
    if (error.response) {
      console.log("HTTP status:", error.response.status);
      console.log("Response data:", error.response.data);

      if (error.response.status === 402) {
        // 提示用户订阅已失效,或者跳转到订阅页面
      }
    } else if (error.request) {
      console.error(
        "请求已发出,但浏览器没有向 Axios 暴露响应。",
        "请检查 CORS、TLS 证书、代理和网络连接。"
      );
    } else {
      console.error("创建请求时发生错误:", error.message);
    }
  }
}

正常情况下,402 错误对象应满足:

error.code === "ERR_BAD_REQUEST"
error.response?.status === 402

具体的 code 可能因 Axios 版本或适配器而异,业务逻辑应以 error.response.status 为准。

不要只依赖 JSON.stringify(error)

AxiosError 的部分属性不可枚举,而且它可能实现了自己的 toJSON()。因此,序列化后的内容不一定会完整显示 request 和 response。

调试时应直接查看:

console.log(error);

if (axios.isAxiosError(error)) {
  console.log(error.response);
  console.log(error.response?.status);
  console.log(error.response?.headers);
  console.log(error.response?.data);
}

不过,当前序列化结果中同时出现了 code: "ERR_NETWORK",又没有 status,这仍符合浏览器未向 Axios 提供响应的特征。

validateStatus 不是这里的修复手段

Axios 默认将 4xx 和 5xx 响应作为 Promise rejection,但仍可通过 error.response 读取响应。这通常正是需要的处理方式。

也可以通过 validateStatus 将 402 当作普通结果:

const axiosInstance = axios.create({
  validateStatus: status => status < 500,
});

这样,402 会进入 then:

const response = await axiosInstance.post(url, data);

if (response.status === 402) {
  // 处理订阅问题
}

但 validateStatus 只决定 HTTP 响应进入 then 还是 catch,无法绕过 CORS、证书错误或连接失败。只要错误仍是 ERR_NETWORK,修改这个选项就不会生效。

仍然出现 ERR_NETWORK 时的排查顺序

先在浏览器 Network 面板中检查 OPTIONS 请求:

  • 是否返回 200 或 204。
  • 是否包含 Access-Control-Allow-Origin: http://localhost:5173。
  • Access-Control-Allow-Headers 是否允许 Authorization 和实际使用的 Content-Type。
  • Access-Control-Allow-Methods 是否允许 POST。
  • 实际的 402 响应是否也包含 Access-Control-Allow-Origin。

如果 CORS 头完整,再检查以下问题:

  • https://localhost:7173 的开发证书是否受浏览器信任。
  • 直接打开该 HTTPS 地址时,浏览器是否显示证书警告。
  • HTTPS 页面是否调用了 HTTP API,从而触发 mixed content。
  • 反向代理、防火墙或安全软件是否中断了连接。
  • 请求是否被取消,例如 AbortController 是否提前触发。
  • 前端配置的 API base URL 和端口是否正确。

如果 ASP.NET Core 本地证书有问题,可以先在浏览器中访问 API 地址,确认是否出现证书警告。证书被浏览器拒绝时,XHR 同样只会向 Axios 报告 ERR_NETWORK。

根据现有信息,应先修复 CORS 策略和中间件顺序,并确认 OPTIONS 预检没有被认证或授权拦截。配置正确后,不需要修改 OpenAPI 生成代码,402 状态码就会出现在 error.response.status 中。

备注:内容仅供参考。