Prevent error page action executing directly from a request - error-handling

In asp.net-core we can show user-friendly error pages by adding the StatusCodePages middleware to the pipeine. In Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// code ...
app.UseExceptionHandler("/error/500");
app.UseStatusCodePagesWithReExecute("/error/{0}");
// code ...
}
With above code, when an unhandled exception occurs or a requested resource can not be found, the responses is handled by redirecting to /error/{0}. Framework correctly invokes this action
[Route("[controller]")]
public class ErrorController : Controller
{
[HttpGet("{statusCode}")]
public IActionResult Error(int statusCode)
{
Response.StatusCode = statusCode;
return View("Error", statusCode);
}
}
The problem starts when client directly requests something like ~/error/{int}. For example www.example.com/error/500 or www.example.com/error/400
In these cases again the above action is being invoked (from MVC not StatusCodePages middleware) and client gets a 500 and a 400 response. In my opinion, 404 status code must be returned for all ~/error/{int} requests.
Is there any solution, when client makes a ~/error/{int} request to prevent MVC middleware from invoking the error action?

Use HttpContext.Features.Get<IExceptionHandlerFeature>() to check whether an error has occurred. If one hasn't, then return a 404. Here is an example.
ErrorController.cs
using Microsoft.AspNet.Diagnostics;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Mvc;
[Route("[controller]")]
public class ErrorController : Controller
{
[HttpGet("{statusCode}")]
public IActionResult Error(int statusCode)
{
var feature = HttpContext.Features.Get<IExceptionHandlerFeature>();
if (feature == null || feature.Error == null)
{
var obj = new { message = "Hey. What are you doing here?"};
return new HttpNotFoundObjectResult(obj);
}
return View("Error", statusCode);
}
}
According to the docs (emphasis added),
The HttpContext type... provides an interface for getting and setting these features... Use the pattern shown above for feature detection from middleware or within your application. Calls made to GetFeature will return an instance if the feature is supported, or null otherwise.

Related

Core 7 - Api error handling, model state validation + UseExceptionhandler

I am currently working on implementing some Apis using swagger/swashbuckle in net core 7 and implementing some error handling, I've gone down the route of using an exception handler. With separate endpoints from dev/prod.
E.g. Startup.cs
if (env.IsDevelopment())
{
...details ommited
app.UseExceptionHandler("/dev-error");
}
else
{
...details ommited
app.UseExceptionHandler("/error");
}
ErrorController.cs
[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorController : Controller
{
private ILogger _logger;
public ErrorController(ILogger logger)
{
_logger = logger;
}
[Route("dev-error")]
public IAttempt DevError()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
return Attempt.Fail(exception);
}
[Route("error")]
public IAttempt Error()
{
var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
var exception = context.Error;
_logger.Log(LogLevel.Error, exception, exception.Message);
switch (exception)
{
case UnauthorizedAccessException:
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
return Attempt.Fail("Unauthorised");
default:
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
return Attempt.Fail("Generic Error");
}
}
}
The idea is that all responses are of IAttempt, so that the FE user can check if its succeeded etc. and whether to handle the result or exception in a user friendly way.
This has been working great up until now when I've been implementing Api's that require the model to be validated. I wanted to amend the IAttempt class to provide modelstate feedback, however I have tried many approaches and cant seem to get modelstate validation flow through the exception handler.
I wanted to implement a custom ValidationException that contains the errors which is then handled in these controllers. But when an exception is thrown in either an IActionFilter or when overriding the InvalidModelStateResponseFactory the exception isn't caught by the exception handler.
Is there a work around? Am I missing something?
Alternatively I could define a InvalidModelStateResponseFactory that returns a similar model(IAttempt), but it would be nice for Failed requests to be handled in one place.
Cheers in advance
I think you can make the InvalidModelStateResponseFactory redirect to the ErrorController, sending the required data to create your response
According to your description, I suggest you could consider using the customer action filter to achieve your requirement.
Inside the custom action filter, we could get the model state's results, then you could throw the exception inside it.
More details, you could refer to below codes:
1.Create the custom action filter:
public class CustomValidationActionFilter : IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (!context.ModelState.IsValid)
{
var errorList = context.ModelState.Values
.SelectMany(m => m.Errors)
.Select(m => m.ErrorMessage)
.ToList();
throw new Exception();
}
}
public void OnActionExecuting(ActionExecutingContext context) { }
}
2.Inside the program.cs
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new CustomValidationActionFilter());
});
Then if it thrown the exception, it will go to the controller's error action method, since you set the global exception handler.
I was unnecessarily over complicating things so I have dropped what I attempted to do as in theory responses should be handled accordingly to their response status code rather then the object thats passed in.

How to return HTTP 404

I am building an asp.net core Web API and I need to be able to hide some of the actions in a controller.
I use the following code to return HTTP 404 (Not Found):
[HttpGet]
public IActionResult Index()
{
if(!_isEnabled)
{
return NotFound();
}
However, in my browser I get this result:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-502319d62a6027718d2ee2cb3c9f263f-28de7bfdfb48f2d8-00"
}
I need to make the call as if the controller does not exists and the browser shows this:
How can a Controller returns a "real" HTTP 404 experience as if the controller dos not exists at that route?
Update 1
The answers return a JSON data and response code 404.
I am trying to do something different.
I am trying to hide the controller as if it doesn't exist for security reasons. I like the end user browser see above screenshot (Edge in my example)
Update 2
I changed to the following code:
[HttpGet]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Index()
{
if(!_isEnabled)
{
return StatusCode(StatusCodes.Status404NotFound);
}
and the controller returns the following result:
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.4","title":"Not Found","status":404,"traceId":"00-3275026575270e11a4b1a5ab0817776a-a4777e626460faeb-00"}
The behavior is strange. Is it a new feature in aspnet code 6 ?
Update 3
Here is my middleware setup in the Program.c. It is plain oob setup:
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddAzureWebAppDiagnostics();
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
Solution For Update 1:
Middleware could be your savior here thus can be achived what you are trying to implement.
Controller:
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
// return Ok(NotFound());
return StatusCode(StatusCodes.Status404NotFound);
}
Note: You can choose either of the status pattern.
Middleware:
public class CustomResponseMiddleware
{
private readonly RequestDelegate _next;
public CustomResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext.Response.StatusCode == 404)
{
httpContext.Response.Redirect("/WrongControllerName/WrongAction");
}
await _next(httpContext);
}
}
Note: As you can see, we are checking the controller status code and checking if any 404 decteced. Once the desired status code we will redirect a controller which doesn't exist at all that eventually generate the expected output.
Register Middleware In Program.cs:
app.UseMiddleware<CustomResponseMiddleware>();
Output:

Blazor WASM Http call is not hitting some API endpoints. Receiving index.html instead

I have a Blazor WASM page that need to make a call to get some data from an API. The Blazor app is ASPNetCore hosted, and the hosting app contains the API.
Some of my endpoints work, but some calls throw a Json serialization exception.
Unhandled exception rendering component: '<' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
If I look at the actual response from the server, it looks like it returns the content of index.html from my WASM app.
Example Controller
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class CompanyController : ControllerBase
{
private readonly ApplicationDbContext _context;
public CompanyController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(long id)
{
Company? company = await _context.Companies.FindAsync(id);
if (company == null)
{
return NotFound();
}
return Ok(company);
}
}
Example Blazor Page
#page "/companies/{id:long}"
#attribute [Authorize]
#inject HttpClient Http
#inject NavigationManager Nav
#if (company != null)
{
<div>#company.Name</div>
}
else
{
<div>Loading Company...</div>
}
#code {
private Company? company;
[Parameter]
public long Id { get; set; }
protected override async Task OnInitializedAsync()
{
try
{
company = await Http.GetFromJsonAsync<Company>($"/api/company/{Id}");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
}
In the example above, everything works as expected. But if I make the following two changes, I'll get the Json Exception mentioned above.
Create an identical controller named WorkOrderController. Everything else is identical including pulling the Company data from the database. Only the name of the controller is different.
Change the Http request to company = await Http.GetFromJsonAsync<Company>($"/api/workOrder/{Id}"); in the Blazor page.
Why would some endpoints work, and some wouldn't?
So, the requestUri passed to GetFromJsonAsync must be lowercase. My request was failing because I had a capital "O" in "workOrder".
I am not sure why this is a requirement of the request parameter, but alas, making the path lowercase fixed the issue.

How do you handle failure to authenticate user in a custom AuthenticationHandler?

I have a scenario where an app needs to authenticate a user by calling an API and sending a user token to verify user identity. I started working on a custom authentication handler based on the following tutorials:
Tutorial 1
Tutorial 2
I have a very basic example which right now simply fails the authentication just to make sure it works:
public class SoleAuthenticationHandler : AuthenticationHandler<SoleAuthenticationOptions>
{
private readonly ISoleApiService _soleApiService;
public SoleAuthenticationHandler(
IOptionsMonitor<SoleAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock, ISoleApiService soleApiService)
: base(options, logger, encoder, clock)
{
_soleApiService = soleApiService;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("You are not authorized to access this resource."));
}
}
This works as intended, controller actions decorated with [Authorize] attribute are intercepted and 401 is thrown. My questions are the following:
How am I supposed to handle the 401 once it happens? For example let's say I want to redirect a user to a friendly page that says "you're not authorized please login". Is that something done in the handler or elsewhere? What is the proper process here? Looking at Microsoft docs for the AuthenticationHandler there is a method called BuildRedirectUri but providing that method with a uri does not really change anything - the page still returns a 401.
As it stands now in order for this to work I need to decorate controllers/actions with [Authorize] attribute. Is there a way to do this globally so that I don't have to specifically authorize each controller and/or action?
We had/have the customErrors pages in ASP.NET web forms and MVC 5.x to redirect users automatically to the specified error pages when a certain statusCode is issued:
<customErrors mode="On" defaultRedirect="error">
<error statusCode="404" redirect="error/notfound" />
<error statusCode="403" redirect="error/forbidden" />
</customErrors>
Here in ASP.NET Core we can simulate these pages this way:
First add a new ErrorController to handle specific statusCodes (id's here) and then return custom views for different conditions:
public class ErrorController : Controller
{
private readonly ILogger<ErrorController> _logger;
public ErrorController(ILogger<ErrorController> logger)
{
_logger = logger;
}
public IActionResult Index(int? id)
{
var logBuilder = new StringBuilder();
var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
logBuilder.AppendLine($"Error {id} for {Request.Method} {statusCodeReExecuteFeature?.OriginalPath ?? Request.Path.Value}{Request.QueryString.Value}\n");
var exceptionHandlerFeature = this.HttpContext.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature?.Error != null)
{
var exception = exceptionHandlerFeature.Error;
logBuilder.AppendLine($"<h1>Exception: {exception.Message}</h1>{exception.StackTrace}");
}
foreach (var header in Request.Headers)
{
var headerValues = string.Join(",", value: header.Value);
logBuilder.AppendLine($"{header.Key}: {headerValues}");
}
_logger.LogError(logBuilder.ToString());
if (id == null)
{
return View("Error");
}
switch (id.Value)
{
case 401:
case 403:
return View("AccessDenied");
case 404:
return View("NotFound");
default:
return View("Error");
}
}
}
Now it's time to connect this controller to the built-in error handling middlewares of ASP.NET Core:
public void Configure(IApplicationBuilder app)
{
if (env.IsDevelopment())
{
app.UseDatabaseErrorPage();
app.UseDeveloperExceptionPage();
}
app.UseExceptionHandler("/error/index/500");
app.UseStatusCodePagesWithReExecute("/error/index/{0}");
About your second question, just define your filter/Authorize attribute globally.

Custom error code pages with message

I am trying to create a custom error code page that displays a message I pass to it in my .NET Core MVC 1.1 application. I setup custom error code pages support in the Startup.cs class file and then created a simple view in a controller that does public IActionResult Example1 => NotFound("Some custom error message"). I expected this message to be pushed to the controller however this is not the case. Calling NotFound() without any parameters hits the error controller but as soon as I pass a message through, the controller is never used and a simple text message is displayed.
I could have sworn I used to do this in the past with classic .NET MVC but it has been awhile.
How can I have custom error code pages that display the proper error. I also need the ability in a controller to return the standard text or JSON response during the error for cases when I expect a JSON response (API actions and such). I am assuming there is a way to do this with a attribute but I have yet to find a way to do either of these tasks.
What you could do is something similar to how the StatusCodePages middleware works. That middleware allows a pipeline re-execution model, to allow handling status code errors through the normal MVC pipeline. So when you return a non-successful status code from MVC, the middleware detects that and then re-executes the whole pipeline for a status code error route. That way, you are able to fully design status code errors. But as Chris Pratt already mentioned, those status codes are typically limited to just their code. There is not really a way to add additional details to it.
But what we could do is create our own error handling implementation on top of that re-execution model. For that, we create a CustomErrorResponseMiddleware which basically checks for CustomErrorResponseException exceptions and then re-executes the middleware pipeline for our error handler.
// Custom exceptions that can be thrown within the middleware
public class CustomErrorResponseException : Exception
{
public int StatusCode { get; set; }
public CustomErrorResponseException(string message, int statusCode)
: base(message)
{
StatusCode = statusCode;
}
}
public class NotFoundResponseException : CustomErrorResponseException
{
public NotFoundResponseException(string message)
: base(message, 404)
{ }
}
// Custom context feature, to store information from the exception
public interface ICustomErrorResponseFeature
{
int StatusCode { get; set; }
string StatusMessage { get; set; }
}
public class CustomErrorResponseFeature : ICustomErrorResponseFeature
{
public int StatusCode { get; set; }
public string StatusMessage { get; set; }
}
// Middleware implementation
public class CustomErrorResponseMiddleware
{
private readonly RequestDelegate _next;
private readonly string _requestPath;
public CustomErrorResponseMiddleware(RequestDelegate next, string requestPath)
{
_next = next;
_requestPath = requestPath;
}
public async Task Invoke(HttpContext context)
{
try
{
// run the pipeline normally
await _next(context);
}
catch (CustomErrorResponseException ex)
{
// store error information to be retrieved in the custom handler
context.Features.Set<ICustomErrorResponseFeature>(new CustomErrorResponseFeature
{
StatusCode = ex.StatusCode,
StatusMessage = ex.Message,
});
// backup original request data
var originalPath = context.Request.Path;
var originalQueryString = context.Request.QueryString;
// set new request data for re-execution
context.Request.Path = _requestPath;
context.Request.QueryString = QueryString.Empty;
try
{
// re-execute middleware pipeline
await _next(context);
}
finally
{
// restore original request data
context.Request.Path = originalPath;
context.Request.QueryString = originalQueryString;
}
}
}
}
Now, all we need to do is hook that up. So we add the middleware within our Startup.Configure, somewhere near the beginning:
app.UseMiddleware<CustomErrorResponseMiddleware>("/custom-error-response");
The /custom-error-response is the route that we are re-executing when a custom response is being requested. This can be a normal MVC controller action:
[Route("/custom-error-response")]
public IActionResult CustomErrorResponse()
{
var customErrorResponseFeature = HttpContext.Features.Get<ICustomErrorResponseFeature>();
var view = View(customErrorResponseFeature);
view.StatusCode = customErrorResponseFeature.StatusCode;
return view;
}
Since this uses MVC, this also needs a view:
#model ICustomErrorResponseFeature
#{
ViewData["Title"] = "Error";
}
<p>There was an error with your request:</p>
<p>#Model.StatusMessage</p>
And that’s basically all. Now, we can just throw our custom error response exceptions from our MVC actions to trigger this:
// generate a 404
throw new NotFoundResponseException("This item could not be found");
// or completely custom
throw new CustomErrorResponseException("This did not work", 400);
Of course, we could also expand this further, but that should be the basic idea.
If you are already using the StatusCodePages middleware, you might think whether all this custom re-execution is really necessary, when you already have exactly that in the StatusCodePages middleware. And well, it is not. We can also just expand on that directly.
For that, we will just add the context features, which we can set at any point during the normal execution. Then, we just return a status code, and let the StatusCodePages middleware run. Inside its handler, we can then look for our feature and use the information there to expand the status code error page:
// Custom context feature
public interface IStatusCodePagesInfoFeature
{
string StatusMessage { get; set; }
}
public class StatusCodePagesInfoFeature : IStatusCodePagesInfoFeature
{
public string StatusMessage { get; set; }
}
// registration of the StatusCodePages middleware inside Startup.Configure
app.UseStatusCodePagesWithReExecute("/error/{0}");
// and the MVC action for that URL
[Route("/error/{code}")]
public IActionResult StatusCode(int code)
{
var statusCodePagesInfoFeature = HttpContext.Features.Get<IStatusCodePagesInfoFeature>();
return View(model: statusCodePagesInfoFeature?.StatusMessage);
}
Inside of the normal controller actions, we can set that feature before returning a status code:
HttpContext.Features.Set<IStatusCodePagesInfoFeature>(new StatusCodePagesInfoFeature
{
StatusMessage = "This item could not be found"
});
return NotFound();
It is too bad you cannot intercept NotFound, Unauthorized, etc. responses in a middleware class.
Okay, option three! You can totally intercept those responses, just not inside of middleware, since these are MVC results and will not leave the MVC pipeline. So you have to intercept them within the MVC filter pipeline. But we could absolutely run a filter, for example a result filter, that modifies the result.
The problem is that we still need a way to pass the information on. We could use a context feature again, but we can also use the MVC object results. So the idea is that we can just do the following in the MVC actions:
return NotFound("The item was not found");
So usually, that string would be the plain text response. But before the result is being executed and the response is being generated, we can run a result filter to modify this and return a view result instead.
public class StatusCodeResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// retrieve a typed controller, so we can reuse its data
if (context.Controller is Controller controller)
{
// intercept the NotFoundObjectResult
if (context.Result is NotFoundObjectResult notFoundResult)
{
// set the model, or other view data
controller.ViewData.Model = notFoundResult.Value;
// replace the result by a view result
context.Result = new ViewResult()
{
StatusCode = 404,
ViewName = "Views/Errors/NotFound.cshtml",
ViewData = controller.ViewData,
TempData = controller.TempData,
};
}
// intercept other results here…
}
await next();
}
}
All you need is a view at Views/Errors/NotFound.cshtml now and everything will magically work once you have the filter registered.
You can either register the filter by adding a [TypeFilter(typeof(StatusCodeResultFilter))] attribute to the controller or individual actions, or you can register it globally.
What you want is not possible. When you do something like return NotFound with a message, that message will be included in the response body only if it's left unmolested. When you do something like enable status code pages, the NotFound is simply caught by the middleware, and the request will simply be handed off to your error handling action to ultimately obtain the response. Importantly, that means your original NotFoundResult along with any custom message has been round-filed.