Handling a 404 error in ASP.NET Core 5 with a custom response class
Asked Answered
S

2

6

I'm working on a Web API in ASP.NET Core 5. I want to handle errors within my Web API.

My service has this code. HttpException is a custom Exception I made so I can have control over the Status Code.

public Users execute(int id)
{
    var foundUser = this.userRepository.findById(id);
    if (foundUser == null)
    {
        throw new HttpException(HttpStatusCode.NotFound, "User not found");
    }
    return foundUser;
}
using System;
using System.Net;

namespace dotnetex.shared.Errors
{
    public class HttpException : Exception
    {

        public HttpStatusCode Status { get; set; }

        public HttpException(HttpStatusCode status, string msg) : base(msg)
        {
            Status = status;
        }
    }
}

My startup class has the Exception Handler pointing to my route:

//This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandler("/error"); // Add this
    [...]
}

And my route looks like this:

using System.Net;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace dotnetex.shared.Errors.Controller
{
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error()
        {
            var exception = HttpContext.Features.Get<IExceptionHandlerFeature>();
            HttpException error = (HttpException)exception.Error;
            var statusCode = (int)error.Status;
            return Problem(detail: error.Message, statusCode: statusCode);
        }
    }
}

For some reason, when I get the statusCode from the "error" variable, I get my response broken, and I see:

Error: Transferred a partial file

Debugging shows me the statusCode variable is set. When I set by code a number, things work as intended.

I've tried to call the Endpoint In Insomnia, Postman, Chrome, and CURL. All of them show errors.

I want to return from this route a custom error object named "API Error" instead of the "problem", like this one:

namespace dotnetex.shared.Errors
{
    public class APIError
    {
        private int status_code = 500;
        private string message = "";
        public APIError(int status_code, string message)
        {
            this.status_code = status_code;
            this.message = message;
        }
    }
}

The exception in console is:

fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
      An unhandled exception has occurred while executing the request.
      dotnetex.shared.Errors.HttpException: User not found
         at dotnetex.modules.users.Services.Implementations.GetUserByIdService.GetUserByIdService.execute(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Services/Implementations/GetUserByIdService/GetUserByIdService.cs:line 23
         at modules.users.Services.UserServices.GetUserById(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Services/UserServices.cs:line 45
         at modules.users.Controllers.UsersControllers.getSingleUser(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Controllers/UsersControllers.cs:line 53
         at lambda_method1(Closure , Object , Object[] )
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
warn: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[4]
      No exception handler was found, rethrowing original exception.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HM6S5CSRT4C8", Request id "0HM6S5CSRT4C8:00000002": An unhandled exception was thrown by the application.
      dotnetex.shared.Errors.HttpException: User not found
         at dotnetex.modules.users.Services.Implementations.GetUserByIdService.GetUserByIdService.execute(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Services/Implementations/GetUserByIdService/GetUserByIdService.cs:line 23
         at modules.users.Services.UserServices.GetUserById(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Services/UserServices.cs:line 45
         at modules.users.Controllers.UsersControllers.getSingleUser(Int32 id) in /home/matt/Source/net/dotnetexSl/dotnetex/modules/users/Controllers/UsersControllers.cs:line 53
         at lambda_method1(Closure , Object , Object[] )
         at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
         at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.HandleException(HttpContext context, ExceptionDispatchInfo edi)
         at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
Slaton answered 28/2, 2021 at 21:6 Comment(3)
my Insomnia tells me Error: Transferred a partial file - so that's possibly an error of your Insomnia. I don't see why there is such error in this simple code you posted. It happens in the complexity of Insomnia. So there may be a lot of other code involved in here. The exception handler feature can just provide you an exception that's set before. That exception object contains just data, no way for an exception to be thrown when accessing that data (which should be readonly). You may have to call for support from Insomnia.T
There are errors in Postman, opening the endpoint in Web Browser, and even in CURL. It's not related to Insomnia. But I've added details in that sentence, thanks.Slaton
actually I thought Insomnia was some web framework built on top of asp.net core, it sounds familiar to me. If so, looks like something wrong with one of the middlewares or cross-cutting code. It's hard to say. You can only try debugging it yourself, try narrowing it down or reproducing it in another place ... From what you posted, I don't see any possibilities for such a strange error.T
P
13

ASP.NET Core 5 introduced a breaking change in handling the response status code of 404 Not Found. Use yjr ExceptionHandlerOptions.AllowStatusCode404Response property.

You can fix the UseExceptionHandler method like this:

Startup.cs

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 {
    app.UseExceptionHandler(
          new ExceptionHandlerOptions()
          {
              AllowStatusCode404Response = true, // important!
              ExceptionHandlingPath = "/error"                  
          }
      );      
  }
Paw answered 8/6, 2021 at 17:32 Comment(0)
B
0

I'm not exactly sure what you're asking here. If you're using an Error Controller and throwing custom exceptions though I would check what kind of exception that is on your controller.

So for example if you define a custom HttpException and then throw that exception like so Throw new HttpException(HttpStatusCodes.NotFound, "Woah something happened!); that would then get caught on your Error Controller.

By default your normal Exception that you catch won't have a status property on it, so you need to handle it something like this:

[Route("error")]
public ErrorResponseModel Error()
{
    var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    var exception = context.Error;

    // Handle if the exception is a HttpException
    if(exception is HttpException httpException)
    {
         Response.StatusCode = (int)httpException.status;

         return new ErrorResponseModel(httpException);
    }

    // Handle all other exceptions
    Response.StatusCode = (int)HttpStatusCode.InternalServerError;

    return new ErrorResponseModel(exception);
}

You can see instead of returning a IActionResult I simply define my own error model, and then set the Response.StatusCode to whatever I want.

This should get you started.

Brok answered 17/4, 2021 at 23:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.