How to handle dynamic error pages in .net MVC Core?
Asked Answered
K

1

9

Currently I have

app.UseExceptionHandler("/Home/Error");

I want to make the path relative to the original path.

For example if

Tenant1/PageThatThrowsError then app.UseExceptionHandler("Tenant1/Home/Error");

but if

Tenant2/PageThatThrowsError then app.UseExceptionHandler("Tenant2/Home/Error");

I thought I would be able to do

app.UseExceptionHandler(
    new ExceptionHandlerOptions
    {
        ExceptionHandler = async (ctx) =>
        {
            //logic that extracts tenant
            ctx.Request.Path = new PathString(Invariant($"{tenant}/Home/Error"));
        }
    }
);

but this throws a 500

EDIT: All the current solutions that for example uses redirects loses the current error context and does not allow the controller to for example call HttpContext.Features.Get().

Korns answered 23/3, 2020 at 13:40 Comment(5)
I am not sure if this will help learn.microsoft.com/en-us/aspnet/core/fundamentals/…Transalpine
I would probably just handle the redirecting (assuming that you have an error page for every tenant) in the error method that is called by the default exception handler, or you can do like the above link shows and create the response dynamically.Transalpine
The error handler does a bunch of stuff out of the box like passing in the error code and so on. IT would be nice to still have that and just be able to set the url dynamically.Korns
does your application listen to Tenant1/Home/Error and Tenant2/Home/Error?Bennybenoit
Yes. But the problem is routing itKorns
B
5

We suppose that the application has required routes and endpoints of /Tenant1/Home/Error and /Tenant2/Home/Error. You can solve the issue using this code:

app.UseExceptionHandler(
    new ExceptionHandlerOptions
    {
        ExceptionHandler = async (ctx) =>
        {
            string tenant = ctx.Request.Host.Value.Split('/')[0];
            ctx.Response.Redirect($"/{tenant}/Home/Error");
        },
    }
);

Another equivalent solution is putting the following code on the startup.cs:

app.UseExceptionHandler("$/{tenant}/Home/Error");

We suppose that tenant comes from somewhere like appsettings. Then you can easily get exceptions on your desired endpoint by writing a simple route on your action:

[Route("/{TenantId}/Home/Error")]
public IActionResult Error(string TenantId)
{
    string Id = TenantId;
    // Here you can write your logic and decide what to do based on TenantId
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

or you can create two different actions:

[Route("/Tenant1/Home/Error")]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
[Route("/Tenant2/Home/Error")]
public IActionResult Error()
{
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

Update:

If your tenants are dynamically added and can't be put in your appsettings.json (what we've supposed in the above solutions) you can write a middle-ware to handle the Exceptions, here is how:

Add the middle-ware in your Startup.cs in Configure method:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));

At the next line add a route for errors (exactly after the middle-ware):

app.UseMvc(routes =>
    {
       routes.MapRoute(
            name: "errors",
            template: "{tenant}/{controller=Home}/{action=Index}/");
    });

Create a class for your middle-ware, and put these code on:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex,this.next);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex, RequestDelegate next)
    {
        string tenant = "tenant1";//write your logic something like this: context.Request.Path.Value.Split('/')[0];
        context.Request.Path = new PathString($"/{tenant}/Home/Error");
        context.Request.HttpContext.Features.Set<Exception>(ex);// add any object you want to the context
        return next.Invoke(context);
    }
}

Note that you can add anything you want to the context like this: context.Request.HttpContext.Features.Set<Exception>(ex);.

And finally you should create an action with an appropriate routing to write your logic there:

[Route("/{TenantId}/Home/Error")]
public IActionResult Error(string TenantId)
{
    string Id = TenantId;
    var exception= HttpContext.Features.Get<Exception>();// you can get the object which was set on the middle-ware
    return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}

Note that the object which was set on the middle-ware, now can be retrieved.

Bennybenoit answered 26/3, 2020 at 20:52 Comment(11)
HttpContext.Features.Get<IExceptionHandlerPathFeature>() use to be available inside the Error controller. Using the redirect losses that context and you can no longer retrieve it.Korns
@Korns What about the second solution? the second solution seems working.Bennybenoit
My tenants are dynamic hence I cannot staticly add the route.Korns
The route app.UseExceptionHandler($"/{tenant}/Home/Error"); is not static! You can put what ever you want instead of {tenant}, and with the route of Route("/{TenantId}/Home/Error")] you can listen to all of them.Bennybenoit
Ok but how does .net know which tenant to route to with app.UseExceptionHandler($"/{tenant}/Home/Error"). The question asks how to infer it from the incoming url that raised the error.?Korns
It is clear that .net routes Tenant1/PageThatThrowsError to Tenant1/Home/Error and Tenant2/PageThatThrowsError to Tenant2/Home/Error and whatever/PageThatThrowsError to whatever/Home/Error.Bennybenoit
app.UseExceptionHandler("/Home/Error"); tells the .asp net where to navigate for error handling. app.UseExceptionHandler($"/{tenant}/Home/Error"); does not. You are missing the part where you actually tell asp .net where to take the user in case of error based on the incoming url WITHOUT losing the context of the error (as with your first suggestion)Korns
Thats the main part that's the key to the question.Korns
Did you run all the tenant in a single application?Bennybenoit
@Korns Please check my update, I've added another solution.Bennybenoit
I would just amend it to context.Request.HttpContext.Features.Set<IExceptionHandlerPathFeature>(new ExceptionHandlerFeature { Error = ex, Path = originalPath });Korns

© 2022 - 2024 — McMap. All rights reserved.