Asp.net core exception handling in applications that combines Razor Pages and ApiControllers - asp.net-core

when using asp.net applications that combines Razor Pages and Api Controllers.
how to globally check if the exception is thrown from an Api Controller ?
the idea is to use UseExceptionHandler midlleware but conditionally return an html response if the unhanded exception is thrown from a Razor Page and a json ProblemDetails response if the excpetion is thrown from an ApiController

For web Api, add attribute route with Api, and then check the request path in the middleware or exception handler like this:
app.UseExceptionHandler("/Error"); //handle the exception from the razor page
//handle the exception from the API.
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), subApp =>
{
subApp.UseExceptionHandler(builder =>
{
builder.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"error\":\"Exception from API!\"}");
//await context.Response.WriteAsync("ERROR From API!<br><br>\r\n");
//await context.Response.WriteAsync("Home<br>\r\n");
//await context.Response.WriteAsync("</body></html>\r\n");
});
});
});
The result as below:
Besides, you can also use a custom exception handler page is to provide a lambda to UseExceptionHandler. Using a lambda allows access to the path of the request that made the error before returning the response.
For example:
//app.UseExceptionHandler("/Home/Error");
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
//check if the handler path contains api or not.
if (exceptionHandlerPathFeature.Path.Contains("api"))
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; ;
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
await context.Response.WriteAsync("ERROR From API!<br><br>\r\n");
await context.Response.WriteAsync(
"Home<br>\r\n");
await context.Response.WriteAsync("</body></html>\r\n");
}
else
{
context.Response.Redirect("/Home/Error");
}
});
});
More detail information, see asp.net core app.UseExceptionHandler() to handle exceptions for certain endpoints?

Related

Middleware writes response when starting up the project

Using ASP.Net Core, C#
I have a middleware where i check for particular cookie is present otherwise returning a 400 response. My problem is the middleware fires up starting the project itself and check the cookie is present or not and then shows the response text in the swagger index page, which i dont want.the middleware fies when swagger loads. I want this condition to be executed only for the requests.
public async Task InvokeAsync(HttpContext context)
{
var pl = context.Request.Cookies["pl"];
var sig = context.Request.Cookies["sig"];
if (string.IsNullOrWhiteSpace(pl) || string.IsNullOrWhiteSpace(sig))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("Invalid Data");
return;
}
// If success i process and do something
// Call the next delegate/middleware in the pipeline
await _next(context);
}
I can prevent firing the middleware when swagger loads and fire only for api requests. But is this better approach or any other better are there.
app.UseWhen(context => `context.Request.Path.ToString().Contains("/api"),HandleBranch);`

ASP.NET Core modify heads on exception

If I throw a BadLabUpdateException then I'd like the response headers of the request to include the exception details. I tried to do this in my Configure method:
app.UseExceptionHandler(errorApp => {
errorApp.Run(async context => {
var pathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (pathFeature is BadLabUpdateException ex) {
context.Response.StatusCode = 400;
context.Response.Headers.Add("X-Display-To-User", new StringValues(ex.Message));
}
});
});
That doesn't have any effect though.
The goal is essentially if I throw that specific exception from the server, then the message will contain the exact string that the client should display to the end user.
I need this to work both in development mode and production.

.NET Core 3 (preview) Web API return UnAuthorized (401) instead of NotFound (404)

Having read through many posts, blogs and this SO thread, this code doesn't do what I expect it to do:
services.AddAuthentication().AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = context =>
{
context.Response.Clear();
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
};
});
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Clear();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
});
An excerpt from an API controller (using the authorize attribute):
[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "User")]
public class TravelPlanController : BaseController{
...
}
This is part of the startup configuration of a Web API in .NET Core 3.x (preview) and should return a 401 UnAuthorized (which essentially should be UnAuthenticated) but instead returns a 404 NotFound.
The 404 results from the fact that the default .NET Core Authentication Middleware redirects to something like /auth/login and that route is not available (by design; it is an API not a MVC website). So the request is unauthorized, gets redirected by default, and results in a 404 :s
Both the apporaches of OnRedirectToLogin handlers should intercept this default behaviour, which is odd for a RESTfull API, and return a simple 401 UnAuthorized. But they don't, breakpoint isn't hit in debug mode, Postman and an Angular app in Chrome both report a 404.
Did anything change since .NET Core 3.x? Or did the solutions from others never really work.
This did the trick:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityContext>()
.AddDefaultTokenProviders()
.AddRoles<IdentityRole>();
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
Working with ASP.NET Core 3.x (preview) and the default Identity provider the order in which the above declarations are specified makes the difference for the OnRedirectToLogin event to be fired.
I eloborated on this in this SO thread as well.
Try to define the OnRedirectToAccessDenied on the Events as you did for OnRedirectToLogin.

How to customize the authorization error produced by OpenIddict?

I'm using OpenIddict for auth in a .NET Core 2 API. Client side I'm relying on any API errors to follow a custom scheme. However, when e.g. a refresh token has been outdated, I can't seem to find out how to customize the error sent back.
The /token endpoint is never reached, so the error is not under "my control".
The result of the request is a status code 400, with the following JSON:
{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
How can I properly customize the error or intercept to change it? Thanks!
You can use OpenIddict's event model to customize the token response payloads before they are written to the response stream. Here's an example:
MyApplyTokenResponseHandler.cs
public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
{
public ValueTask HandleAsync(ApplyTokenResponseContext context)
{
var response = context.Response;
if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
!string.IsNullOrEmpty(response.ErrorDescription))
{
response.ErrorDescription = "Your customized error";
}
return default;
}
}
Startup.cs
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
options.AddEventHandler<ApplyTokenResponseContext>(builder =>
builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
})
.AddValidation();
The /token endpoint is never reached, so the error is not under "my control".
In fact ,the /token is reached, and the parameter of grant_type equals refresh_token. But the rejection logic when refresh token expired is not processed by us. It is some kind of "hardcoded" in source code :
if (token == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(token))
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
}
The context.Reject here comes from the assembly AspNet.Security.OpenIdConnect.Server.
For more details, see source code on GitHub .
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
I've tried and I'm pretty sure we can use a custom middleware to catch all status codes. The key point is to detect the status code after the next() invocation:
app.Use(async(context , next )=>{
// passby all other end points
if(! context.Request.Path.StartsWithSegments("/connect/token")){
await next();
return;
}
// since we might want to detect the Response.Body, I add some stream here .
// if you only want to detect the status code , there's no need to use these streams
Stream originalStream = context.Response.Body;
var hijackedStream = new MemoryStream();
context.Response.Body = hijackedStream;
hijackedStream.Seek(0,SeekOrigin.Begin);
await next();
// if status code not 400 , pass by
if(context.Response.StatusCode != 400){
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
return;
}
// read and custom the stream
hijackedStream.Seek(0,SeekOrigin.Begin);
using (StreamReader sr = new StreamReader(hijackedStream))
{
var raw= sr.ReadToEnd();
if(raw.Contains("The specified refresh token is no longer valid.")){
// custom your own response
context.Response.StatusCode = 401;
// ...
//context.Response.Body = ... /
}else{
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
}
}
});
// helper to make the copy easy
private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
newStream.Seek(0,SeekOrigin.Begin);
await newStream.CopyToAsync(originalStream);
context.Response.ContentLength =originalStream.Length;
context.Response.Body = originalStream;
}

Custom 404 response model

I want to provide a custom reponse for all 404s on our API. For example:
{
"message": "The requested resource does not exist. Please visit our documentation.."
}
I believe the following result filter works for all cases within the MVC pipeline:
public class NotFoundResultFilter : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var result = context.Result as NotFoundResult;
if (result != null)
{
context.Result = new HttpNotFoundResult(); // My custom 404 result object
}
}
}
But, when a URL requested does not match an action route, the above filter is not hit. How could I best intercept these 404 responses? Would this require middleware?
Yes, you need to use middleware, as filter is only for MVC.
You may, as always, write your own middleware
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 404)
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject("your text"), Encoding.UTF8);
}
});
Or use built-in middlware StatusCodePagesMiddleware, but as you want to handle only one status, this is an extra functionality. This middleware can be used to handle the response status code is between 400 and 600 .You can configure the StatusCodePagesMiddleware adding one of the following line to the Configure method (example from StatusCodePages Sample):
app.UseStatusCodePages(); // There is a default response but any of the following can be used to change the behavior.
// app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
// app.UseStatusCodePages("text/plain", "Response, status code: {0}");
// app.UseStatusCodePagesWithRedirects("~/errors/{0}"); // PathBase relative
// app.UseStatusCodePagesWithRedirects("/base/errors/{0}"); // Absolute
// app.UseStatusCodePages(builder => builder.UseWelcomePage());
// app.UseStatusCodePagesWithReExecute("/errors/{0}");
Try this:
app.Use( async ( context, next ) =>
{
await next();
if ( context.Response is { StatusCode: 404, Body: { Length: 0 }, HasStarted: false } )
{
context.Response.ContentType = "application/problem+json; charset=utf-8";
string jsonString = JsonConvert.SerializeObject(errorDTO);
await context.Response.WriteAsync( jsonString, Encoding.UTF8 );
}
} );