How to wrap a route not found exception with the generic response model

Aakash Bashyal 1 Reputation point
2021-07-06T03:44:05.377+00:00

I am trying to capture the exception when the route is not found and wrap the exception with the generic response model. I tried to implement, as given in the answer to the question, but this solution also doesn't seem to work in my use case.

The status code 404 is also added to the response when the resource is not found, like when Id is not found or some object is null.

 app.UseStatusCodePages(new StatusCodePagesOptions()
 {
     HandleAsync = (ctx) =>
     {
          if (ctx.HttpContext.Response.StatusCode == 404)
          {
                throw new RouteNotFoundException("Route not found");
          }

          return Task.FromResult(0);
     }
 })

RouteNotFoundException

public class RouteNotFoundException : Exception
{
    public RouteNotFoundException()
          : base()
    {
    }

    public RouteNotFoundException(string message)
        : base(message)
    {
    }
}

ApiExceptionFilterAttribute

public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
        private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;

        public ApiExceptionFilterAttribute()
        {
            // Register known exception types and handlers.
            _exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
            {
                { typeof(RouteNotFoundException), HandleNotFoundException }
            };
        }
        public override void OnException(ExceptionContext context)
        {
            HandleException(context);

            base.OnException(context);
        }
        private void HandleException(ExceptionContext context)
        {
            Type type = context.Exception.GetType();
            if (_exceptionHandlers.ContainsKey(type))
            {
                _exceptionHandlers[type].Invoke(context);
                return;
            }

            HandleUnknownException(context);
        }
        private void HandleNotFoundException(ExceptionContext context)
        {
            var exception = context.Exception as RouteNotFoundException;

            var details = new ProblemDetails()
            {
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
                Title = "The specified resource was not found.",
                Detail = exception.Message
            };

            context.Result = new NotFoundObjectResult(details);
            context.ExceptionHandled = true;
        }
        private void HandleUnknownException(ExceptionContext context)
        {
            var details = new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "An error occurred while processing your request.",
                Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
            };

            context.Result = new ObjectResult(details)
            {
                StatusCode = StatusCodes.Status500InternalServerError
            };

            context.ExceptionHandled = true;
        }
}

ResponseWrapperMiddleware

public class ResponseWrapperMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ResponseWrapperMiddleware> _logger;

    public ResponseWrapperMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _logger = loggerFactory?.CreateLogger<ResponseWrapperMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
    }

    public async Task Invoke(HttpContext httpContext)
    {

        try
        {
            var currentBody = httpContext.Response.Body;

            using (var memoryStream = new MemoryStream())
            {
                //set the current response to the memorystream.
                httpContext.Response.Body = memoryStream;

                await _next(httpContext);

                //reset the body 
                httpContext.Response.Body = currentBody;
                memoryStream.Seek(0, SeekOrigin.Begin);

                var readToEnd = new StreamReader(memoryStream).ReadToEnd();
                var objResult = JsonConvert.DeserializeObject(readToEnd);
                var result = CommonApiResponse.Create((HttpStatusCode)httpContext.Response.StatusCode, objResult, null);
                await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(result));
            }
        }
        catch (Exception ex)
        {
            if (httpContext.Response.HasStarted)
            {
                _logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
                throw;
            }
            return;
        }
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class ResponseWrapperMiddlewareExtensions
{
    public static IApplicationBuilder UseResponseWrapperMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ResponseWrapperMiddleware>();
    }
}

Generic Response Model

public class CommonApiResponse
{
    public static CommonApiResponse Create(HttpStatusCode statusCode, object result = null, string errorMessage = null)
    {
        return new CommonApiResponse(statusCode, result, errorMessage);
    }

    public string Version => "1.2.3";

    public int StatusCode { get; set; }
    public string RequestId { get; }

    public string ErrorMessage { get; set; }

    public object Result { get; set; }

    protected CommonApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
    {
        RequestId = Guid.NewGuid().ToString();
        StatusCode = (int)statusCode;
        Result = result;
        ErrorMessage = errorMessage;
    }
}

How to handle the error if the route is not found and capture the error in the generic model? What is the workaround for this case?

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,573 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Zhi Lv - MSFT 32,336 Reputation points Microsoft Vendor
    2021-07-07T08:50:41.847+00:00

    Hi @Aakash Bashyal ,

    From your description, you want to capture the 404 error (caused by the incorrect URL or the resource is not found,) and format the response with the generic response model, right?

    For the 404 errors caused by incorrect routing or request URLs, you could use the UseStatusCodePages middleware.

    Code as below:

    app.UseStatusCodePages(new StatusCodePagesOptions()  
    {  
        HandleAsync = async (ctx) =>  
        {  
            if (ctx.HttpContext.Response.StatusCode == 404)  
            {  
                await ctx.HttpContext.Response.WriteAsJsonAsync(new RouteNotFoundException("Route not found"));  
            }  
                       
        }  
    });  
    

    For the 404 errors caused by the resource is not found, in the action method, you could use try-catch statement to capture the exception, then throw a RouteNotFoundException, then use exception handler to handle the exception, code as below:

    Action method:

    // GET api/<ValuesController>/5  
    [HttpGet("{id}")]  
    public string Get(int id)  
    {  
        throw new RouteNotFoundException("File not found exception thrown in API Action method");  
        return "value";  
    }  
    

    Startup.Configure method:

    app.UseExceptionHandler(errorApp =>  
    {  
        errorApp.Run(async context =>  
        {  
    
            var exceptionHandlerPathFeature =  
                context.Features.Get<IExceptionHandlerPathFeature>();  
    
            if (exceptionHandlerPathFeature?.Error is RouteNotFoundException)  
            {  
                await context.Response.WriteAsJsonAsync(new RouteNotFoundException(exceptionHandlerPathFeature.Error.Message));  
            }  
            else  
            {  
                context.Response.Redirect("/Home/Error");  
            }  
        });  
    });  
    

    The result as below:

    112449-image.png

    Besides, you could also create a middleware to handle the exception:

    Create a ErrorHandlerMiddleware middleware with the following code:

    public class ErrorHandlerMiddleware  
    {  
        private readonly RequestDelegate _next;  
    
        public ErrorHandlerMiddleware(RequestDelegate next)  
        {  
            _next = next;  
        }  
    
        public async Task Invoke(HttpContext context)  
        {  
            try  
            {  
                await _next(context);  
            }  
            catch (Exception error)  
            {  
                var response = context.Response;  
                response.ContentType = "application/json";  
    
                switch (error)  
                {  
                    case RouteNotFoundException e:  
                        // custom application error  
                        response.StatusCode = (int)HttpStatusCode.BadRequest;  
                        break;  
                    case KeyNotFoundException e:  
                        // not found error  
                        response.StatusCode = (int)HttpStatusCode.NotFound;  
                        break;  
                    default:  
                        // unhandled error  
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;  
                        break;  
                }  
    
                var result = JsonSerializer.Serialize(new RouteNotFoundException(error?.Message));  
                await response.WriteAsync(result);  
            }  
        }  
    }  
    

    Register the middleware in the Startup.Configure method:

      app.UseMiddleware<ErrorHandlerMiddleware>();  
    

    The result as below:

    112449-image.png

    The Startup.Configure method as below:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
    {  
        if (env.IsDevelopment())  
        {  
            app.UseDeveloperExceptionPage();  
        }  
        else  
        {  
            //app.UseExceptionHandler("/Home/Error");  
            ////use UseExceptionHandler method to handle the exception  
            //app.UseExceptionHandler(errorApp =>  
            //{  
            //    errorApp.Run(async context =>  
            //    {  
      
            //        var exceptionHandlerPathFeature =  
            //            context.Features.Get<IExceptionHandlerPathFeature>();  
      
            //        if (exceptionHandlerPathFeature?.Error is RouteNotFoundException)  
            //        {  
            //            await context.Response.WriteAsJsonAsync(new RouteNotFoundException(exceptionHandlerPathFeature.Error.Message));  
            //        }  
            //        else  
            //        {  
            //            context.Response.Redirect("/Home/Error");  
            //        }  
            //    });  
            //});  
      
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.  
            app.UseHsts();  
        }  
        app.UseHttpsRedirection();  
        app.UseStaticFiles();  
           
        app.UseStatusCodePages(new StatusCodePagesOptions()  
        {  
            HandleAsync = async (ctx) =>  
            {  
                if (ctx.HttpContext.Response.StatusCode == 404)  
                {  
                    await ctx.HttpContext.Response.WriteAsJsonAsync(new RouteNotFoundException("Route not found"));  
                }  
                   
            }  
        });  
      
        app.UseRouting();  
      
        app.UseAuthorization();  
        app.UseSession();  
        app.UseMiddleware<ErrorHandlerMiddleware>();   
        app.UseEndpoints(endpoints =>  
        {  
            endpoints.MapControllerRoute(  
                name: "default",  
                pattern: "{controller=Home}/{action=Index}/{id?}");   
        });  
    }  
    

    Reference: Exception handler lambda


    If the answer is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best Regards,
    Dillion


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.