Foxnouns.NET/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs

166 lines
5.7 KiB
C#

// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Net;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Utils;
using Newtonsoft.Json;
namespace Foxnouns.Backend.Middleware;
public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
try
{
await next(ctx);
}
catch (Exception e)
{
Type type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware);
string typeName = e.TargetSite?.DeclaringType?.FullName ?? "<unknown>";
ILogger logger = baseLogger.ForContext(type);
if (ctx.Response.HasStarted)
{
logger.Error(
e,
"Error in {ClassName} ({Path}) after response started being sent",
typeName,
ctx.Request.Path
);
sentry.CaptureException(
e,
scope =>
{
User? user = ctx.GetUser();
if (user != null)
{
scope.User = new SentryUser
{
Id = user.Id.ToString(),
Username = user.Username,
};
}
}
);
return;
}
if (e is ApiError ae)
{
ctx.Response.StatusCode = (int)ae.StatusCode;
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
ctx.Response.ContentType = "application/json; charset=utf-8";
if (ae is ApiError.Forbidden fe)
{
await ctx.Response.WriteAsync(
JsonConvert.SerializeObject(
new HttpApiError
{
Status = (int)fe.StatusCode,
Code = ErrorCode.Forbidden,
Message = fe.Message,
Scopes = fe.Scopes.Length > 0 ? fe.Scopes : null,
}
)
);
return;
}
if (ae is ApiError.BadRequest br)
{
await ctx.Response.WriteAsync(br.ToJson().ToString());
return;
}
await ctx.Response.WriteAsync(
JsonConvert.SerializeObject(
new HttpApiError
{
Status = (int)ae.StatusCode,
Code = ae.ErrorCode,
Message = ae.Message,
}
)
);
return;
}
if (e is FoxnounsError fce)
{
logger.Error(
fce.Inner ?? fce,
"Exception in {ClassName} ({Path})",
typeName,
ctx.Request.Path
);
}
else
{
logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path);
}
SentryId errorId = sentry.CaptureException(
e,
scope =>
{
User? user = ctx.GetUser();
if (user != null)
{
scope.User = new SentryUser
{
Id = user.Id.ToString(),
Username = user.Username,
};
}
}
);
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
ctx.Response.Headers.RequestId = ctx.TraceIdentifier;
ctx.Response.ContentType = "application/json; charset=utf-8";
await ctx.Response.WriteAsync(
JsonConvert.SerializeObject(
new HttpApiError
{
Status = (int)HttpStatusCode.InternalServerError,
Code = ErrorCode.InternalServerError,
ErrorId = errorId.ToString(),
Message = "Internal server error",
}
)
);
}
}
}
public record HttpApiError
{
public required int Status { get; init; }
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
public required ErrorCode Code { get; init; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? ErrorId { get; init; }
public required string Message { get; init; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string[]? Scopes { get; init; }
}