Angular+.Net Core生产环境API被路由拦截报错问题排查
问题现象
部署C# .NET Core后端+Angular前端的同站点到IIS生产环境后,第三方支付平台的回调接口https://www.mysite.example/api/jccpayment/JCCResponse(用户完成/取消支付后,支付平台会通过浏览器跳转发起该请求)被Angular路由拦截,控制台抛出如下错误:
Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'api/jccpayment/JCCResponse' Error: Cannot match any routes. URL Segment: 'api/jccpayment/JCCResponse' at Jt.noMatchError
该问题仅在生产环境出现,本地开发环境运行完全正常:用户在支付平台操作时长在30秒内时,回调请求可正常命中后端接口;超过30秒必现上述路由错误。
已尝试以下方案均未生效:
- 已配置页面base标签为
<base href="/"> - 尝试切换Hash路由策略
{useHash: true},也显式配置过PathLocationStrategy{ provide: LocationStrategy, useClass: PathLocationStrategy },问题依旧 - 排查过HTTP请求超时配置,确认是路由层面的拦截问题,非接口请求本身超时断开
现有核心配置
Angular路由配置(app-routing.module.ts)
export const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'folder/:id', loadChildren: () => import('./folder/folder.module').then( m => m.FolderPageModule) }, { path: 'pages/test/test', loadChildren: () => import('./pages/test/test.module').then( m => m.TestPageModule) }, { path: 'pages/test2/test2', loadChildren: () => import('./pages/test2/test2.module').then( m => m.Test2PageModule) }, { path: 'tc-form', loadChildren: () => import('./pages/tc-form/tc-form.module').then( m => m.TcFormPageModule) }, { path: 'tc-driver-list', loadChildren: () => import('./pages/tc-driver-list/tc-driver-list.module').then( m => m.TcDriverListPageModule) }, { path: 'login', loadChildren: () => import('./pages/login/login.module').then( m => m.LoginPageModule) }, { path: 'verification', loadChildren: () => import('./pages/verification/verification.module').then( m => m.VerificationPageModule) }, { path: 'tc-formlist', loadChildren: () => import('./pages/tc-formlist/tc-formlist.module').then( m => m.TcFormlistPageModule) }, { path: 'jcc-depositlist', loadChildren: () => import('./pages/jcc-depositlist/jcc-depositlist.module').then(m => m.JCCDepositlistPageModule) }, { path: 'pricelist', loadChildren: () => import('./pages/pricelist/pricelist.module').then(m => m.PricelistPageModule) }, { path: 'tc-checkout', loadChildren: () => import('./pages/tc-checkout/tc-checkout.module').then( m => m.TcCheckoutPageModule) }, { path: 'jcc-complete', loadChildren: () => import('./pages/jcc-complete/jcc-complete.module').then( m => m.JccCompletePageModule) }, { path: 'jcc-request', loadChildren: () => import('./components-global/jcc-request/jcc-request.module').then( m => m.JccRequestPageModule) }, { path: 'jcc-response', loadChildren: () => import('./components-global/jcc-response/jcc-response.module').then( m => m.JccResponsePageModule) }, { path: 'covernote-document', loadChildren: () => import('./pages/covernote-document/covernote-document.module').then( m => m.CovernoteDocumentPageModule) }, { path: 'register', loadChildren: () => import('./pages/register/register.module').then( m => m.RegisterPageModule) }, { path: 'forgot-password', loadChildren: () => import('./pages/forgot-password/forgot-password.module').then( m => m.ForgotPasswordPageModule) }, { path: 'forgot-password2', loadChildren: () => import('./pages/forgot-password2/forgot-password2.module').then( m => m.ForgotPassword2PageModule) }, { path: 'version-modal', loadChildren: () => import('./pages/version-modal/version-modal.module').then( m => m.VersionModalPageModule) }, { path: 'home', loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule) }, { path: 'alteration', loadChildren: () => import('./pages/alteration/alteration.module').then( m => m.AlterationPageModule) }, { path: 'jcc-wait', loadChildren: () => import('./components-global/jcc-wait/jcc-wait.module').then( m => m.JccWaitPageModule) } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [RouterModule] }) export class AppRoutingModule {}
.NET Core 管道配置(Startup.cs)
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.Use(async (context, next) => { await next(); if (context.Response.StatusCode == 404 && !System.IO.Path.HasExtension(context.Request.Path.Value)) { context.Request.Path = "/index.html"; await next(); } }); app.UseMiddleware(typeof(ErrorHandlingMiddleware)); } else { app.Use(async (context, next) => { await next(); if (context.Response.StatusCode == 404 && !System.IO.Path.HasExtension(context.Request.Path.Value) && !context.Request.Path.Value.Contains("/api/")) { context.Request.Path = "/index.html"; await next(); } }); app.UseMiddleware(typeof(ErrorHandlingMiddleware)); app.UseHsts(); } app.UseDefaultFiles(); app.UseStaticFiles(); if (!env.IsDevelopment()) { app.UseSpaStaticFiles(); } app.UseRouting(); app.UseSession(); app.UseHttpsRedirection(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); }
根因分析
问题触发有两个核心前提,刚好对应30秒阈值、仅生产环境复现的特征:
- IIS托管ASP.NET Core时,AspNetCoreModuleV2模块默认的请求超时时间为30秒,当支付回调接口处理时长超过30秒时,模块会直接中断后端请求返回非404的错误状态码,不会走到正常的API响应逻辑。
- 原有SPA回退逻辑仅判断了404状态下排除
/api/路径,没有处理超时返回的其他错误状态码;同时UseSpa中间件内置的回退逻辑会把所有未被后端正常处理的非静态文件请求重写到index.html,/api开头的超时请求被重写到Angular入口页后,Angular路由找不到对应前端路由就会抛出匹配错误。
本地开发环境走Kestrel+Angular Cli代理,没有IIS模块的30秒超时限制,API请求也不会触发SPA回退逻辑,因此完全正常。
修复方案
按以下两步修改即可彻底解决:
- 调整IIS AspNetCore模块的请求超时配置,覆盖默认30秒的限制,适配支付回调的长处理场景。修改发布后站点目录下的
web.config,在aspNetCore节点添加requestTimeout属性,示例配置如下:
<system.webServer> <handlers> <remove name="aspNetCore"/> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="dotnet" arguments=".\你的项目dll名.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" requestTimeout="00:10:00"/> </system.webServer>
上述配置把请求超时调整为10分钟,可根据实际业务场景调整时长
- 修正生产环境的SPA回退中间件逻辑,所有
/api/前缀的请求无论返回什么状态码,都禁止重写到index.html,替换原有生产环境的自定义中间件代码:
app.Use(async (context, next) => { await next(); // 所有/api开头的请求直接返回,不做SPA回退 if (context.Request.Path.StartsWithSegments("/api")) { return; } // 非API的404请求才重写到index.html if (context.Response.StatusCode == 404 && !System.IO.Path.HasExtension(context.Request.Path.Value)) { context.Request.Path = "/index.html"; await next(); } });
修改完成后重启IIS站点即可生效,无论回调接口处理多长时间,都不会再被Angular路由拦截。
内容的提问来源于stack exchange,提问作者DepCharis
相关产品推荐
相关产品推荐

