Skip to content

Commit

Permalink
Add structured error mapper (#507)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaliumhexacyanoferrat authored Jul 1, 2024
1 parent 658962f commit 576b6f5
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 6 deletions.
14 changes: 12 additions & 2 deletions Modules/ErrorHandling/ErrorHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using GenHTTP.Api.Content;

using GenHTTP.Modules.Conversion.Providers;
using GenHTTP.Modules.ErrorHandling.Provider;

namespace GenHTTP.Modules.ErrorHandling
Expand All @@ -16,10 +17,19 @@ public static class ErrorHandler
/// </summary>
/// <remarks>
/// By default, server errors will be rendered into
/// a HTML template.
/// structured responses.
/// </remarks>
/// <returns>The default error handler</returns>
public static ErrorSentryBuilder<Exception> Default() => Html();
public static ErrorSentryBuilder<Exception> Default() => Structured();

/// <summary>
/// Ans error handler which will render exceptions into
/// structured error objects serialized to the format
/// requested by the client (e.g. JSON or XML).
/// </summary>
/// <param name="serialization">The serialization configuration to be used</param>
/// <returns>A structured error handler</returns>
public static ErrorSentryBuilder<Exception> Structured(SerializationBuilder? serialization = null) => From(new StructuredErrorMapper(serialization?.Build()));

/// <summary>
/// An error handler which will render exceptions into
Expand Down
1 change: 1 addition & 0 deletions Modules/ErrorHandling/GenHTTP.Modules.ErrorHandling.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<ProjectReference Include="..\..\API\GenHTTP.Api.csproj" />

<ProjectReference Include="..\Pages\GenHTTP.Modules.Pages.csproj" />
<ProjectReference Include="..\Conversion\GenHTTP.Modules.Conversion.csproj" />

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />

Expand Down
5 changes: 2 additions & 3 deletions Modules/ErrorHandling/Provider/ErrorSentry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ public ErrorSentry(IHandler parent, Func<IHandler, IHandler> contentFactory, IEr
{
try
{
var response = await Content.HandleAsync(request)
;

var response = await Content.HandleAsync(request);

if (response is null)
{
return await ErrorHandler.GetNotFound(request, Content);
Expand Down
85 changes: 85 additions & 0 deletions Modules/ErrorHandling/StructuredErrorMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Threading.Tasks;

using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.Conversion;
using GenHTTP.Modules.Conversion.Providers;
using GenHTTP.Modules.Conversion.Providers.Json;

namespace GenHTTP.Modules.ErrorHandling
{

public sealed class StructuredErrorMapper : IErrorMapper<Exception>
{

#region Supporting data structures

public record class ErrorModel(ResponseStatus Status, string Message, string? StackTrace = null);

#endregion

#region Get-/Setters

public SerializationRegistry Registry { get; }

#endregion

#region Initialization

public StructuredErrorMapper(SerializationRegistry? registry)
{
Registry = registry ?? Serialization.Default().Build();
}

#endregion

#region Functionality

public async ValueTask<IResponse?> Map(IRequest request, IHandler handler, Exception error)
{
string? stackTrace = null;

if (request.Server.Development)
{
stackTrace = error.StackTrace;
}

if (error is ProviderException e)
{
var model = new ErrorModel(e.Status, error.Message, stackTrace);

return await RenderAsync(request, model);
}
else
{
var model = new ErrorModel(ResponseStatus.InternalServerError, error.Message, stackTrace);

return await RenderAsync(request, model);
}
}

public async ValueTask<IResponse?> GetNotFound(IRequest request, IHandler handler)
{
var model = new ErrorModel(ResponseStatus.NotFound, "The requested resource does not exist on this server");

return await RenderAsync(request, model);
}

private async ValueTask<IResponse> RenderAsync(IRequest request, ErrorModel model)
{
var format = Registry.GetSerialization(request) ?? new JsonFormat();

var response = await format.SerializeAsync(request, model);

response.Status(model.Status);

return response.Build();
}

#endregion

}

}
4 changes: 3 additions & 1 deletion Testing/Acceptance/Engine/DeveloperModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public async Task TestExceptionsWithTrace()

using var response = await runner.GetResponseAsync();

Assert.IsTrue((await response.GetContentAsync()).Contains("Exception"));
var content = await response.GetContentAsync();

Assert.IsTrue(content.Contains("HandleAsync"));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Net;
using System.Threading.Tasks;

using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.ErrorHandling;
using GenHTTP.Modules.Functional;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace GenHTTP.Testing.Acceptance.Modules.ErrorHandling
{

[TestClass]
public sealed class StructuredErrorMapperTests
{

[TestMethod]
public async Task TestNotFound()
{
using var host = TestHost.Run(Inline.Create());

using var response = await host.GetResponseAsync();

await response.AssertStatusAsync(HttpStatusCode.NotFound);

var model = await response.GetContentAsync<StructuredErrorMapper.ErrorModel>();

Assert.AreEqual(ResponseStatus.NotFound, model.Status);

Assert.IsNull(model.StackTrace);
}

[TestMethod]
public async Task TestGeneralError()
{
var handler = Inline.Create()
.Get(() => DoThrow(new Exception("Oops")));

using var host = TestHost.Run(handler);

using var response = await host.GetResponseAsync();

await response.AssertStatusAsync(HttpStatusCode.InternalServerError);

var model = await response.GetContentAsync<StructuredErrorMapper.ErrorModel>();

Assert.AreEqual(ResponseStatus.InternalServerError, model.Status);

Assert.IsNotNull(model.StackTrace);
}

[TestMethod]
public async Task TestProviderError()
{
var handler = Inline.Create()
.Get(() => DoThrow(new ProviderException(ResponseStatus.Locked, "Locked up!")));

using var host = TestHost.Run(handler);

using var response = await host.GetResponseAsync();

await response.AssertStatusAsync(HttpStatusCode.Locked);

var model = await response.GetContentAsync<StructuredErrorMapper.ErrorModel>();

Assert.AreEqual(ResponseStatus.Locked, model.Status);

Assert.IsNotNull(model.StackTrace);
}

[TestMethod]
public async Task TestNoTraceInProduction()
{
var handler = Inline.Create()
.Get(() => DoThrow(new Exception("Oops")));

using var host = TestHost.Run(handler, development: false);

using var response = await host.GetResponseAsync();

await response.AssertStatusAsync(HttpStatusCode.InternalServerError);

var model = await response.GetContentAsync<StructuredErrorMapper.ErrorModel>();

Assert.IsNull(model.StackTrace);
}

private static void DoThrow(Exception e)
{
throw e;
}

}

}

0 comments on commit 576b6f5

Please sign in to comment.