Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

101 lines
3.3 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Net;
using System.Threading.Tasks;
using EpicGames.Core;
using HordeServer.Utilities;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace HordeServer.Server
{
/// <summary>
/// Middleware that logs any unhandled exception and then rethrows it,
/// so upstream handlers (developer exception page, exception filters, etc.) still see it.
/// </summary>
public sealed class ExceptionLoggingMiddleware(RequestDelegate next, ILogger<ExceptionLoggingMiddleware> logger)
{
/// <summary>
/// Name of key to store in HttpContext to track if an exception has been logged
/// </summary>
public const string LoggedContextKey = "ExceptionLogged";
/// <summary>
/// Invokes the next middleware and logs any unhandled exception with request method, path, and trace identifier for correlation.
/// </summary>
/// <exception cref="Exception">
/// Always rethrows the original exception after logging, to preserve default error handling.
/// </exception>
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(ex, "Unhandled exception for {Method} {Path} {TraceId}", context.Request.Method, context.Request.Path, context.TraceIdentifier);
context.Items[LoggedContextKey] = true;
throw;
}
}
}
/// <summary>
/// Controller managing account status
/// </summary>
[ApiController]
[Route("[controller]")]
public class ExceptionController(ILogger<ExceptionController> logger) : HordeControllerBase
{
/// <summary>
/// Outputs a diagnostic error response for an exception
/// </summary>
[Route("/api/v1/exception")]
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult Exception()
{
IExceptionHandlerPathFeature? feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
bool wasAlreadyLogged = HttpContext.Items.ContainsKey(ExceptionLoggingMiddleware.LoggedContextKey);
int statusCode = (int)HttpStatusCode.InternalServerError;
LogEvent logEvent;
if (feature?.Error == null)
{
const string Msg = "Exception handler path feature is missing or no error code.";
logEvent = LogEvent.Create(LogLevel.Error, Msg);
logger.LogError(Msg);
}
else if (feature.Error is StructuredHttpException structuredHttpEx)
{
(logEvent, statusCode) = (structuredHttpEx.ToLogEvent(), structuredHttpEx.StatusCode);
if (!wasAlreadyLogged)
{
logger.Log(logEvent.Level, structuredHttpEx, "Structured HTTP exception: {Message}", structuredHttpEx.Message);
}
}
else if (feature.Error is StructuredException structuredEx)
{
logEvent = structuredEx.ToLogEvent();
if (!wasAlreadyLogged)
{
logger.Log(logEvent.Level, structuredEx, "Structured exception: {Message}", structuredEx.Message);
}
}
else
{
logEvent = LogEvent.Create(LogLevel.Error, default, feature.Error, "Unhandled exception: {Message}", feature.Error.Message);
if (!wasAlreadyLogged)
{
logger.LogError(feature.Error, "Unhandled exception on {Path}: {Message}", feature.Path, feature.Error.Message);
}
}
return new ObjectResult(logEvent) { StatusCode = statusCode };
}
}
}