Angular 如何避免路由拦截 ASP.NET Core API 请求?
结论
这不是 Angular Router 要解决的问题,也不需要改用 HashLocationStrategy。
浏览器出现以下错误:
Cannot match any routes. URL Segment: 'api/jccpayment/JCCResponse'
说明服务器把 /api/jccpayment/JCCResponse 的响应错误地处理成了 Angular 的 index.html。Angular 加载后读取当前地址,把它当作前端路由解析,随后抛出路由无法匹配的错误。
正确的处理方式是:
- 先让 ASP.NET Core 匹配 API 路由。
/api/*请求无论成功、返回 404 或 405,还是发生异常,都不能回退到index.html。- SPA fallback 只处理前端页面的
GET/HEAD请求。 - 检查 IIS 的 URL Rewrite 规则,避免它提前把
/api/*重写到 Angular。 - 记录支付平台实际发送的 HTTP method、状态码和响应内容,确认大约 30 秒后,请求方式是否发生变化,或者后端是否触发了超时逻辑。
为什么错误看起来来自 Angular
Angular Router 只能处理已经加载到浏览器中的 Angular 应用。外部支付系统发给服务器的 HTTP POST 请求,不会先经过浏览器里的 Angular Router。
实际过程通常是:
支付平台 POST /api/jccpayment/JCCResponse
↓
ASP.NET Core 或 IIS 没有匹配到该请求
↓
SPA fallback 返回 index.html
↓
浏览器加载 Angular
↓
Angular 尝试解析 /api/jccpayment/JCCResponse
↓
Cannot match any routes
所以,Angular 报错只是结果,根因仍在服务器端。无论是在 Angular 路由表中添加 /api、配置通配路由,还是改用 #,都无法修复服务器端的路由问题。
推荐的 ASP.NET Core 管道配置
以下示例适用于使用 endpoint routing 的 ASP.NET Core 项目。先映射 controller,最后再配置 SPA fallback。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (!env.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
// 如果项目还使用传统 controller/action 路由,可以保留。
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
// 必须最后注册,只用于 Angular 页面地址。
endpoints.MapFallbackToFile("index.html");
});
}
API controller 需要明确声明路由和 HTTP method:
[ApiController]
[Route("api/[controller]")]
public class JccPaymentController : ControllerBase
{
[HttpPost("JCCResponse")]
public IActionResult JCCResponse([FromForm] JccPaymentResponse model)
{
// 验证支付平台签名,并以订单号实现幂等处理。
return Ok();
}
}
如果支付平台发送 JSON,应改用 [FromBody]:
[HttpPost("JCCResponse")]
public IActionResult JCCResponse([FromBody] JccPaymentResponse model)
{
return Ok();
}
选择 [FromForm] 还是 [FromBody],要以支付平台实际发送的 Content-Type 为准,不能只根据本地测试结果判断。
不要用 404 中间件重写 API 请求
问题中的自定义中间件会先等待后续组件执行。当响应为 404 时,它会修改请求路径,再次调用 next():
await next();
if (context.Response.StatusCode == 404)
{
context.Request.Path = "/index.html";
await next();
}
这种写法容易引发以下问题:
- API 路由没有匹配时,服务器可能返回 Angular 页面;
- POST 请求也可能进入 SPA fallback;
- 再次调用后续管道时,部分中间件或 endpoint routing 未必会按预期重新匹配;
- 真实的 404、405 或认证错误可能被 Angular 页面掩盖;
- 日志中可能只剩下最终的
200 text/html,看不到 API 最初返回的错误。
建议删除这段手动回退逻辑,改用 MapFallbackToFile("index.html")。
如果旧版项目结构必须保留自定义 fallback,至少要同时限制请求方法和路径:
app.Use(async (context, next) =>
{
await next();
bool isPageRequest =
HttpMethods.IsGet(context.Request.Method) ||
HttpMethods.IsHead(context.Request.Method);
bool isApiRequest =
context.Request.Path.StartsWithSegments(
"/api",
StringComparison.OrdinalIgnoreCase);
bool hasExtension =
Path.HasExtension(context.Request.Path.Value);
if (context.Response.StatusCode == StatusCodes.Status404NotFound &&
isPageRequest &&
!isApiRequest &&
!hasExtension &&
!context.Response.HasStarted)
{
context.Request.Path = "/index.html";
await next();
}
});
即使加上这些限制,自定义中间件仍不如 endpoint fallback 清楚可靠。
不要使用:
context.Request.Path.Value.Contains("/api/")
判断路径时应使用 StartsWithSegments,同时处理大小写:
context.Request.Path.StartsWithSegments(
"/api",
StringComparison.OrdinalIgnoreCase)
UseSpa 也必须放在 API 路由之后
旧式项目可能使用:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
});
UseSpa 通常是末端中间件,因此必须放在 controller 路由之后:
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
不要等到 UseSpa 之后才注册 API controller。没有匹配到路由的 API 请求,也不应由 SPA 返回 index.html。
检查 IIS 的 URL Rewrite 配置
如果 ASP.NET Core 管道已经配置正确,但 production 环境仍然返回 Angular 页面,需要检查部署后的 web.config。IIS 可能在请求到达 ASP.NET Core 之前,就把所有不带扩展名的地址重写到了 index.html。
如果项目确实需要通过 IIS Rewrite 处理前端路由,规则必须排除 /api,并且最好只处理 GET 请求:
<rewrite>
<rules>
<rule name="Angular Routes" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_METHOD}" pattern="^GET$" />
<add input="{REQUEST_URI}" pattern="^/api(?:/|$)" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
如果 Angular fallback 已经由 ASP.NET Core 的 MapFallbackToFile 或 UseSpa 处理,通常不必再配置一套 IIS SPA rewrite。两层 fallback 同时存在,会让路由行为更难判断。
还需要确认 /api/* 最终交给 ASP.NET Core Module 处理,而不是被 IIS 静态文件处理器、代理规则或其他站点规则截获。
为什么大约 30 秒后才失败
Angular Router 没有等待 30 秒后接管 API 请求的机制。这个时间规律更可能来自以下环节:
- 支付平台的支付会话或返回令牌过期;
- 支付平台在超时前后使用了不同的 HTTP method;
- 超时后的请求缺少某个表单字段、Cookie、签名或状态参数;
- controller 因状态校验失败返回 404;
- 接口只定义了
[HttpPost],但支付平台在超时后改用GET,导致服务器返回 405; - IIS、反向代理或应用中的超时规则改变了请求处理结果;
- controller 抛出异常后,错误处理中间件将响应转成了 SPA fallback;
- 应用依赖临时内存、Session 或浏览器 Cookie,而这些状态在回调时已经失效。
排查时不要只看浏览器控制台。应在 IIS 和 ASP.NET Core 的入口处记录请求:
app.Use(async (context, next) =>
{
var startedAt = DateTimeOffset.UtcNow;
await next();
var elapsed = DateTimeOffset.UtcNow - startedAt;
logger.LogInformation(
"Payment callback request: {Method} {Path}{QueryString}, " +
"Status={StatusCode}, ContentType={ContentType}, ElapsedMs={ElapsedMs}",
context.Request.Method,
context.Request.Path,
context.Request.QueryString,
context.Response.StatusCode,
context.Response.ContentType,
elapsed.TotalMilliseconds);
});
对比 30 秒前后的日志时,重点检查:
Request.MethodRequest.PathContent-Type- HTTP 状态码
- 响应
Content-Type - controller 是否执行
- 支付平台提交的字段名称
- 签名、订单号或状态参数是否存在
- IIS 日志中的
sc-status、sc-substatus和sc-win32-status
如果失败响应是:
HTTP/1.1 200 OK
Content-Type: text/html
并且响应正文是 Angular 的 index.html,基本可以确认请求进入了错误的 fallback。
如果响应是 404,说明服务器没有匹配到 endpoint,或者 controller 主动返回了 NotFound()。
如果响应是 405 Method Not Allowed,需要核对支付平台实际使用的 method 是否与 [HttpPost] 一致。
浏览器返回地址和服务器通知地址应尽量分开
不少支付平台会区分两类 URL:
- 浏览器返回地址:支付完成后,将用户导航回网站。
- 服务器通知地址:支付平台的服务器向商户服务器发送支付结果。
如果支付平台支持这种配置,建议分别设置:
浏览器返回:
https://www.mysite.example/jcc-complete
服务器回调:
https://www.mysite.example/api/jccpayment/JCCResponse
/jcc-complete 是 Angular 页面,只负责显示“正在确认支付”或最终结果。/api/jccpayment/JCCResponse 是后端接口,负责验证签名并更新订单状态。
不要只依靠浏览器返回请求确认付款。用户可能关闭页面,网络也可能中断,返回请求还可能被重复提交。更新支付状态时应处理:
- 服务端签名验证;
- 金额和币种校验;
- 订单号校验;
- 幂等处理;
- 重复通知处理;
- 必要时主动向支付平台查询最终状态。
Angular 端不需要添加 /api 路由
以下配置都无法解决这个问题:
RouterModule.forRoot(routes, { useHash: true })
{ provide: LocationStrategy, useClass: HashLocationStrategy }
{
path: 'api/**',
component: SomeComponent
}
Angular 路由只负责页面导航。/api 必须在 IIS、反向代理和 ASP.NET Core 请求管道中与前端路由隔离。
可以继续使用:
{ provide: LocationStrategy, useClass: PathLocationStrategy }
前提是服务器只对真正的前端页面执行 SPA fallback。
最终检查清单
部署后,先验证支付回调:
curl -i -X POST \
https://www.mysite.example/api/jccpayment/JCCResponse \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "orderId=test&status=cancelled"
然后验证一个不存在的 API:
curl -i https://www.mysite.example/api/not-existing
第二个请求应返回真正的 404,不能返回 Angular 的 index.html。
最后验证 Angular 页面:
curl -i https://www.mysite.example/jcc-complete
只有这个请求才应该返回:
Content-Type: text/html
处理原则是:controller 路由优先,SPA fallback 放在最后;API 永远不能回退到 Angular;IIS 不应提前重写 /api/*。满足这三点后,可以继续使用 PathLocationStrategy,支付回调也不会再因加载 index.html 而进入 Angular Router。
备注:内容仅供参考。
