// 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 { /// /// Middleware that logs any unhandled exception and then rethrows it, /// so upstream handlers (developer exception page, exception filters, etc.) still see it. /// public sealed class ExceptionLoggingMiddleware(RequestDelegate next, ILogger logger) { /// /// Name of key to store in HttpContext to track if an exception has been logged /// public const string LoggedContextKey = "ExceptionLogged"; /// /// Invokes the next middleware and logs any unhandled exception with request method, path, and trace identifier for correlation. /// /// /// Always rethrows the original exception after logging, to preserve default error handling. /// 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; } } } /// /// Controller managing account status /// [ApiController] [Route("[controller]")] public class ExceptionController(ILogger logger) : HordeControllerBase { /// /// Outputs a diagnostic error response for an exception /// [Route("/api/v1/exception")] [ApiExplorerSettings(IgnoreApi = true)] public ActionResult Exception() { IExceptionHandlerPathFeature? feature = HttpContext.Features.Get(); 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 }; } } }