Angular + .NET Core生产环境下浏览器调用API的路由异常问题
错误信息
Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'api/tcform/GetCurrentDate'
Error: Cannot match any routes. URL Segment: 'api/tcform/GetCurrentDate'
at Jt.noMatchError (https://mysite/main.3a68a3e49f0763cc89ec.js:1:265122)
at o.selector (https://mysite/main.3a68a3e49f0763cc89ec.js:1:264852)
at o.error(https://mysite/main.3a68a3e49f0763cc89ec.js:1:894743)
at o._error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884678)
at o.error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884475)
at o._error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884678)
at o.error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884475)
at F._error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884678)
at F.error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884475)
at I._error (https://mysite/main.3a68a3e49f0763cc89ec.js:1:884678)
问题描述
生产环境中,等待30秒以上再调用API会触发上述路由匹配错误;30秒内调用则正常运行,该问题已持续一个月。
相关细节
- 仅在Production构建版本中出现
- 托管于IIS服务器
- 构建发布流程:执行
npm run build -- --configuration production后将dist文件夹内容复制到wwwroot,再通过VS2019发布部署到IIS - 未使用hash路由策略,尝试启用后问题仍存在
附Startup.cs代码
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MotoYdrogiosVS.Repository; using MotoYdrogiosVS.Middleware; using MotoYdrogiosVS.Services.DataProtector; using Microsoft.IdentityModel.Tokens; using System.Text; using System; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace MyNamespace { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddSingleton<IGlobalVariablesRepository, GlobalVariablesRepository>(); services.AddSingleton<IProtectorService, ProtectorService>(); //var provider = services.BuildServiceProvider(); //var cipher = provider.GetService<IProtectorService>(); services.AddScoped<IAuthenticationRepository, AuthenticationRepository>(); services.AddScoped<ITcAccountsRepository, TcAccountsRepository>(); services.AddScoped<IErrorRepository, ErrorRepository>(); services.AddScoped<IPriceTableRepository, PriceTableRepository>(); services.AddScoped<IReportsRepository, ReportsRepository>(); services.AddScoped<ITcDriverFormRepository, TcDriverFormRepository>(); services.AddScoped<ITcFormRepository, TcFormRepository>(); services.AddScoped<IVehiclesMakeRepository, VehiclesMakeRepository>(); services.AddScoped<IJCCRepository, JCCRepository>(); services.AddScoped<ISystemRepository, SystemRepository>(); //services.AddEntityFrameworkSqlServer(); //commented out by charis at 27/6/2022 services.AddMvc().AddNewtonsoftJson(); services.AddMvcCore(); // In production, the Angular files will be served from this directory services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; //configuration.RootPath = "ClientApp/dist"; }); services.AddSession(); } // 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.Request.Path.Value.Contains("api")) // { // 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)); //app.UseDeveloperExceptionPage(); } else { //app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.Use(async (context, next) => { //var url = context.Request.Path.Value; //if (url.Contains("/api/tcform/GetCurrentDate")) //{ // // ...do your things // context.Response.Redirect(url, permanent: false); // return; // short circuit //} 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.UseStaticFiles(); //app.UseDefaultFiles(); app.UseStaticFiles(); //app.UseDefaultFiles(); //app.UseStaticFiles(new StaticFileOptions //{ // OnPrepareResponse = context => // { // if (context.File.Name == "index.html") // { // context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store"); // context.Context.Response.Headers.Add("Expires", "-1"); // } // } //}); if (!env.IsDevelopment()) { //app.UseMvc(); //app.UseStatusCodePagesWithReExecute("/"); // <-- added to redirect to angular app.UseSpaStaticFiles(new StaticFileOptions() { OnPrepareResponse = ctx => { var headers = ctx.Context.Response.GetTypedHeaders(); headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromDays(0) }; } }); } app.UseRouting(); app.UseSession(); app.UseHttpsRedirection(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); }); app.UseSpa(spa => { // To learn more about options for serving an Angular SPA from ASP.NET Core, // see https://go.microsoft.com/fwlink/?linkid=864501 spa.Options.SourcePath = "ClientApp"; spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions() { OnPrepareResponse = ctx => { var headers = ctx.Context.Response.GetTypedHeaders(); headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromDays(0) }; } }; //spa.Options.StartupTimeout = new TimeSpan(0, 10, 0); if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); } } }
问题原因分析
- 中间件顺序错误:
UseHttpsRedirection、UseStaticFiles等中间件的执行顺序不符合ASP.NET Core规范,导致API请求未被后端路由正确处理,反而可能被重定向到前端页面,触发Angular路由错误。 - IIS应用池闲置超时:若应用池闲置超时设置为30秒,30秒无请求后应用池会被回收,再次请求时应用重启,第一个API请求可能因后端服务未完全初始化返回404,进而被前端路由接管。
- 自定义404重写逻辑漏洞:判断API路径的逻辑不够严谨,可能导致部分API请求被错误重定向到index.html。
解决方法
1. 修正中间件执行顺序
调整Configure方法中的中间件顺序,遵循ASP.NET Core规范:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseMiddleware(typeof(ErrorHandlingMiddleware)); } else { app.UseHsts(); app.UseMiddleware(typeof(ErrorHandlingMiddleware)); } // HTTPS重定向放在最前面 app.UseHttpsRedirection(); // 静态文件服务 app.UseStaticFiles(); if (!env.IsDevelopment()) { app.UseSpaStaticFiles(new StaticFileOptions() { OnPrepareResponse = ctx => { var headers = ctx.Context.Response.GetTypedHeaders(); headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromDays(0) }; } }); } // 路由与Session app.UseRouting(); app.UseSession(); // 端点路由 app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); }); // 自定义404重写移到端点路由之后 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.StartsWithSegments("/api")) { context.Request.Path = "/index.html"; await next(); } }); } app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions() { OnPrepareResponse = ctx => { var headers = ctx.Context.Response.GetTypedHeaders(); headers.CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromDays(0) }; } }; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } }); }
2. 调整IIS应用池设置
- 打开IIS管理器,找到目标应用池,进入“高级设置”
- 将“闲置超时(分钟)”改为20分钟(默认值)或更长时间,避免频繁回收
- 检查“回收”选项,取消短时间内的自动回收配置
3. 完善Session配置
在ConfigureServices中明确Session超时时间:
services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // 根据业务需求调整 options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; });
4. 优化前端API错误处理
在Angular的HTTP拦截器中捕获API请求错误,避免Angular路由处理API路径:
@Injectable() export class HttpErrorInterceptor implements HttpInterceptor { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 404 && request.url.includes('/api/')) { // 处理API 404错误,比如提示用户或重新请求 return throwError(() => new Error('服务暂时不可用,请稍后重试')); } return throwError(() => error); }) ); } }
内容的提问来源于stack exchange,提问作者DepCharis

