3

I have a .NET Core web app which has an API. I've defined an Middleware class based on this answer like so:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    private readonly ILogger logger;

    public ErrorHandlingMiddleware(RequestDelegate next,
        ILoggerFactory loggerFactory)
    {
        this.next = next;
        logger = loggerFactory.CreateLogger<ErrorHandlingMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(0, ex, "An unhandled exception has occurred: " + ex.StackTrace);
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var code = HttpStatusCode.InternalServerError;
        var message = exception.Message;
        if (exception is BadRequestException)
        {
            code = HttpStatusCode.BadRequest;
        }
        else if (exception is NotFoundException)
        {
            code = HttpStatusCode.NotFound;
        }
        else if (exception is NotAuthorizedException)
        {
            code = HttpStatusCode.Forbidden;
        }
        else if (exception is NotAuthenticatedException)
        {
            code = HttpStatusCode.Unauthorized;
        }
        else
        {
            message = "An unexpected error occurred.";
        }

        var result = JsonConvert.SerializeObject(new { error = message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

The error handling only handles when an exception is thrown in code. A bad route does not throw an exception. The problem is that if I try to access a non-existent API route - that is, one that follows the API route convention and starts with "/api/adfasdf" - the API returns HTML (or the error page or the home page, I forget).

I've received some suggestions to check the context.Response.StatusCode after await next(context); executes, but it's 200.

How can I configure my web app such that it recognizes a bad API route and returns a 404?

UPDATE Here is where/when I load the middleware in my Startup class:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime, IOptions<OidcConfig> oidcConfigOptions)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    // Add Serilog to the logging pipeline
    loggerFactory.AddSerilog();

    app.UseMiddleware<ErrorHandlingMiddleware>();

    if (env.IsLocal())
    {
        app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
        {
            HotModuleReplacement = true
        });
    }

    var oidcConfig = oidcConfigOptions.Value;

    // Configure the app to use Jwt Bearer Authentication
    app.UseJwtBearerAuthentication(new JwtBearerOptions
    {
        AutomaticAuthenticate = true,
        AutomaticChallenge = true,
        Authority = oidcConfig.GetAuthority(),
        Audience = oidcConfig.ResourceAppId,
        TokenValidationParameters = new TokenValidationParameters
        {
            RequireExpirationTime = true,
            RequireSignedTokens = true,
            ValidateAudience = true,
            ValidIssuer = oidcConfig.GetIssuer(),
            ValidateIssuer = true,
            ValidateActor = false,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true
        },
    });

    app.UseSiteIdClaimInjection();

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");

        routes.MapSpaFallbackRoute(
            name: "spa-fallback",
            defaults: new { controller = "Home", action = "Index" });
    });

    appLifetime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}
mellis481
  • 4,332
  • 12
  • 71
  • 118
  • You could try inspecting the `context.Response.StatusCode` right after calling `await next(context);` – Nkosi May 17 '17 at 19:50
  • @Nkosi That's what was suggested in the comments of that answer. It returns a `200`. – mellis481 May 17 '17 at 19:53
  • Don't abuse exception for that/flow control. They are for exceptions not for expected results. – Tseng May 17 '17 at 20:15
  • @im1dermike, How and where in the pipeline do you add the middle ware? this should be added very early, if not, first, to the pipeline. – Nkosi May 18 '17 at 15:25
  • @Nkosi Updated. When I make it the absolute first line in my Configure() method, I still get a 200. – mellis481 May 18 '17 at 15:35

3 Answers3

4

For posterity, the reason I was getting a 200 as @Nkosi helped uncover had to do with an MVC route definition in the Startup class. This came in automatically from https://github.com/aspnet/JavaScriptServices.

The solution was to change my route config to the following:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{
    builder.UseMvc(routes =>
    {
        routes.MapSpaFallbackRoute(
            name: "spa-fallback",
            defaults: new { controller = "Home", action = "Index" });
    });
});
Nkosi
  • 235,767
  • 35
  • 427
  • 472
mellis481
  • 4,332
  • 12
  • 71
  • 118
2

Referencing ASP.NET Core Middleware Fundamentals - Ordering

The order that middleware components are added in the Configure method defines the order in which they are invoked on requests, and the reverse order for the response. This ordering is critical for security, performance, and functionality.

The Configure method (shown below) adds the following middleware components:

  • Exception/error handling
  • Static file server
  • Authentication
  • MVC

C#

public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseIdentity();                     // Authenticate before you access
                                           // secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

In the code above, UseExceptionHandler is the first middleware component added to the pipeline—therefore, it catches any exceptions that occur in later calls.

Based on code provided in OP and quoted documentation I would suggest adding your exception earlier or first to the pipeline.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime) {
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();
    loggerFactory.AddSerilog();

    app.UseMiddleware<ErrorHandlingMiddleware>(); // Call first to catch exceptions
                                                  // thrown in the following middleware.    
    if (env.IsLocal()) {
        app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true });
    }

    //Bunch of other stuff
}

UPDATE Based on comments.

I suspect that one of the middleware further down the pipeline is causing this issue. try removing them one by one and checking if you get the same behavior in order to narrow down which one is the culprit.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I still get a `200` after `await next(context);` from `context.Response.StatusCode`. – mellis481 May 24 '17 at 14:04
  • I suspect that one of the middleware further down the pipeline is causing this issue. try removing them one by one and checking if you get the same behavior in order to narrow down which one is the culprit. – Nkosi May 24 '17 at 14:14
  • 1
    Bingo! It looks like it's the `MapSpaFallbackRoute` line which was added by https://github.com/aspnet/JavaScriptServices. – mellis481 May 24 '17 at 14:21
0

Similar to above answer, We are using this in our Angular and ASP.NET MVC Core project:

        public virtual void Configure(IHostingEnvironment environment, IApplicationBuilder app)
{

            // configurations...

            app.UseMvc(routes =>
            {
                routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
            });

            app.MapWhen(o => !o.Request.Path.Value.StartsWith("/api"), builder =>
            {
                builder.UseMvc(routes =>
                {
                    routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" });
                });
            });

}
F. Badili
  • 261
  • 3
  • 3