ASP.NET Core 5.0 - Not Found Errors

This article will describe how to capture the path and query string of a not found request. I will assume you have downloaded the ASP.NET Core 5.0 - Homegrown Analytics Project or created a new ASP.NET Core 5.0 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core.

Homegrown Analytics Project and Article Series

The project is feature complete and will increment the version with updates. The details page includes the version change log. This project is designed and organized to allow easy integration with existing projects. The KenHaggerty. Com. SingleUser NuGet package segregates the user logic. SQL scripts to create the schema, tables, and default data are included in the package. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Homegrown Analytics Project.

I used to send myself an email every time the Error page was requested. See ASP.NET Core 2.2 - Error and Exception Handling After getting too many emails for not found errors generated by probing robots, I developed a NotFoundError entity and log rather than emailing not found errors.

The UseDeveloperExceptionPage extension certainly aids development but, you should not ignore the user's experience when they encounter an error. The UseStatusCodePagesWithReExecute("/Error/{0}") extension adds a StatusCodePages middleware to the pipeline. Specifies that the response body should be generated by re-executing the request pipeline using an alternate path. This path may contain a '{0}' placeholder of the status code. See MS Docs The project employs a private readonly bool property named debugExceptionHandling to develop the Error page and NotFoundErrors without introducing the UseHsts extension to the development environment.

Startup:
// Used to test Error page and NotFoundErrors.
private readonly bool debugExceptionHandling = false;
Startup > Configure:
var isDevelopment = env.IsDevelopment();
if (isDevelopment && !debugExceptionHandling)
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseStatusCodePagesWithReExecute("/Error/{0}");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    if (!isDevelopment)
        app.UseHsts();
}

The "/Error/{0}" parameter with UseStatusCodePagesWithReExecute extension redirects the user to the Error page. The Error page is configured to accept the StatusCode if provided.

Error.cshtml:
@page "{statusCode?}"
@model ErrorModel
Error.cshtml.cs:
public async Task OnGetAsync(int? statusCode = null)

The project injects the AnalyticsService on the Error page to log NotFoundErrors when the 404 status code is detected. The OriginalPath and OriginalQueryString can be extracted from the IStatusCodeReExecuteFeature. The project implements the IExceptionHandlerFeature to extract exceptions when the status code is not a 404.

Error.cshtml.cs:
public class ErrorModel : PageModel
{
    private readonly IAnalyticsService _analyticsService;
    public ErrorModel(IAnalyticsService analyticsService)
    {
        _analyticsService = analyticsService;
    }

    public string ErrorDetails { get; set; }

    public async Task OnGetAsync(int? statusCode = null)
    {
        if (statusCode == null) statusCode = HttpContext.Response.StatusCode;
        string requestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;            
        IStatusCodeReExecuteFeature feature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
        string originalPath = string.Empty;
        string originalQueryString = string.Empty;
        if (feature != null)
        {
            originalPath = feature.OriginalPath;
            originalQueryString = feature.OriginalQueryString;
        }

        string uaString = HttpContext.Request.Headers["User-Agent"].FirstOrDefault();
        if (string.IsNullOrEmpty(uaString)) uaString = "NullOrEmpty";
        else
            if (uaString.Length > 512) uaString = uaString.Substring(0, 512);

        int userAgentId = await _analyticsService.GetUserAgentIdByUAStringAsync(uaString).ConfigureAwait(false);

        DataAccessResult result;
        if (userAgentId == 0)
        {
            UserAgent userAgent = new UserAgent(uaString);
            result = await _analyticsService.AddUserAgentAsync(userAgent);
            if (!result.Succeeded)
                userAgentId = 1;
            else
            {
                if (userAgent.Id == 0)
                    throw new InvalidOperationException($"An error occurred adding a UserAgent (userAgent.Id = 0).");

                userAgentId = userAgent.Id;
            }
        }

        string anonymizedIp = HttpContext.Connection.RemoteIpAddress.AnonymizeIP();

        if (statusCode == 404)
        {
            if (originalPath.Length > 512) originalPath = originalPath.Substring(0, 512);
            if (originalQueryString != null && originalQueryString.Length > 1024)
                originalQueryString = originalQueryString.Substring(0, 1024);

            NotFoundError notFoundError = new NotFoundError(requestId, originalPath, originalQueryString, userAgentId, anonymizedIp);
            result = await _analyticsService.AddNotFoundErrorAsync(notFoundError).ConfigureAwait(false);
            if (!result.Succeeded)
                throw new InvalidOperationException($"An error occurred adding a NotFoundError ({result}).");
            if (notFoundError.Id == 0)
                throw new InvalidOperationException($"An error occurred adding a NotFoundError (notFoundError.Id = 0).");

            ErrorDetails = "404: NotFoundError has been recorded.";
        }
        else
        {
            StringBuilder sb = new StringBuilder($"An error has occurred. \r\n \r\n");
            sb.Append($"RequestId = {requestId} \r\nStatusCode = {statusCode} \r\n");
            sb.Append($"OriginalPath = {originalPath} \r\n");
            sb.Append($"OriginalQueryString = {originalQueryString} \r\n \r\n");
            sb.Append($"UAString = {uaString} \r\n");
            sb.Append($"AnonymizedIp = {anonymizedIp} \r\n \r\n");

            var exception = HttpContext.Features.Get<IExceptionHandlerFeature>();
            if (exception != null)
            {
                sb.Append($"Error Message = {exception.Error.Message} \r\n");
                sb.Append($"Error Source = {exception.Error.Source} \r\n");
                if (exception.Error.InnerException != null)
                    sb.Append($"Inner Exception = {exception.Error.InnerException} \r\n");
                else
                    sb.Append("Inner Exception = null \r\n");

                sb.Append($"Error StackTrace = {exception.Error.StackTrace} \r\n");
            }
            // Displays sensitive information, consider sending details to a support email.
            ErrorDetails = sb.ToString();
        }
    }
}

The project's Admin/ Analytics/ Index page has a function which will log a NotFoundError for the Development environment if debugExceptionHandling = true. The project's Error page will display the ErrorDetails if not null or empty. This displays sensitive information, consider sending details to a support email.

Error.cshtml:
@if (!string.IsNullOrEmpty(Model.ErrorDetails))
{
    <div class="row border-bottom pb-2 mb-2">
        <div class="col-12">
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">×</span>
                </button>
                @Model.ErrorDetails
            </div>
        </div>
    </div>
}
Admin Analytics Index Page.
Admin Analytics Index Page Mobile.
Error Page.
Error Page Mobile.
Admin NotFoundError Listing Page.
Admin NotFoundError Listing Page Mobile.
Admin NotFoundError Listing Page Wide.
Ken Haggerty
Created 01/02/21
Updated 01/06/21 06:03 GMT

Log In or Reset Quota to read more.

Article Tags:

Analytics EF Core Model
Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?