You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular + .NET Core生产环境下浏览器调用API的路由异常问题

生产环境下延迟30秒调用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");
                }
            });
        }
    }
}

问题原因分析

  1. 中间件顺序错误:UseHttpsRedirection、UseStaticFiles等中间件的执行顺序不符合ASP.NET Core规范,导致API请求未被后端路由正确处理,反而可能被重定向到前端页面,触发Angular路由错误。
  2. IIS应用池闲置超时:若应用池闲置超时设置为30秒,30秒无请求后应用池会被回收,再次请求时应用重启,第一个API请求可能因后端服务未完全初始化返回404,进而被前端路由接管。
  3. 自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 08:06:29