question

AakashBashyal-7213 avatar image
0 Votes"
AakashBashyal-7213 asked ZhiLv-MSFT edited

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

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?










dotnet-aspnet-core-mvc
· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

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.

If the Id is not found or a result set is null, then your code is returning the 404. You have total control over what's returned in the message body. Perhaps read the official docs which explains the option available in asp.net Core Web API.

Return Types
Web API Conventions


0 Votes 0 ·

I think you miss understood my question, I want to catch the exception when the route is not found and add that to the response while returning. I am stuck on checking if the route is valid or not.

0 Votes 0 ·

1 Answer

ZhiLv-MSFT avatar image
0 Votes"
ZhiLv-MSFT answered ZhiLv-MSFT edited

Hi @AakashBashyal-7213,

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


image.png (16.6 KiB)
· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

If there is a wrong controller or the action name it won't hit the action, you are using inside the controller.

> throw new RouteNotFoundException("File not found exception thrown in API Action method");

The route not found error here means if the action, or controller is not found, or there is a wrong route entered from the client application.

  // 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";
  }

here in this controller instead of api/values/2 if the user enters app/values/2 then the route won't match, so it should be captured ahead, and wrap in the response.
`




0 Votes 0 ·

Hi @AakashBashyal-7213,

Yes, if there is a wrong controller or the action name, it means that the request URL is incorrect, it won't hit the action method. In this scenario, it will use the UseStatusCodePages method to return the custom response. So, you should use the UseStatusCodePages method with the UseExceptionHandler or the custom middleware ErrorHandlerMiddlewaretogether.

0 Votes 0 ·