Skip to content

Commit

Permalink
New exception handler (#111)
Browse files Browse the repository at this point in the history
* Added editing product after note recognized by photo

* Fill product form values after note recognized

* Fixed double toggle dialog bug

* Fix not closing dialog after edit

* Added isProblemDetailsError type guard

* Show error message from non successful recognize note response

* Handle OpenAI errors as ProblemDetails

* Refactor error handling, use Result pattern

* Handle empty ProblemDetails and unknown error responses

* Handle no food found on photo case

* Added new exception handler with error handling tests

* Removed unused folder
  • Loading branch information
pkirilin authored Jun 23, 2024
1 parent c16bc40 commit b8245f8
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 117 deletions.
43 changes: 43 additions & 0 deletions src/backend/src/FoodDiary.API/ErrorHandling/ExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.ClientModel;
using System.Threading;
using System.Threading.Tasks;
using FoodDiary.Domain.Exceptions;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace FoodDiary.API.ErrorHandling;

public class ExceptionHandler(
ILogger<ExceptionHandler> logger,
IProblemDetailsService problemDetailsService) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
logger.LogError("{Exception}", exception);

var (statusCode, detail) = exception switch
{
ImportException e => (StatusCodes.Status400BadRequest, e.Message.Trim()),
ClientResultException e => (StatusCodes.Status500InternalServerError, e.Message.Trim()),
_ => (StatusCodes.Status500InternalServerError, "Something went wrong")
};

httpContext.Response.StatusCode = statusCode;

return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = httpContext,
Exception = exception,
ProblemDetails =
{
Title = "An error occurred",
Detail = detail
}
});
}
}

This file was deleted.

7 changes: 4 additions & 3 deletions src/backend/src/FoodDiary.API/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Reflection;
using System.Threading.Tasks;
using FoodDiary.API.ErrorHandling;
using FoodDiary.API.Extensions;
using FoodDiary.API.Logging;
using FoodDiary.API.Middlewares;
using FoodDiary.API.Options;
using FoodDiary.Application.Extensions;
using FoodDiary.Configuration;
Expand Down Expand Up @@ -113,6 +113,7 @@ public void ConfigureServices(IServiceCollection services)

services.AddSerilog((provider, logger) => logger.Configure(provider));
services.AddProblemDetails();
services.AddExceptionHandler<ExceptionHandler>();

services.ConfigureCustomOptions(_configuration);
services.Configure<ImportOptions>(_configuration.GetSection("Import"));
Expand Down Expand Up @@ -152,8 +153,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
c.SwaggerEndpoint("/swagger/v1/swagger.json", "FoodDiary API v1");
});
}

app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseExceptionHandler();
app.UseSpaStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
Expand Down

This file was deleted.

15 changes: 1 addition & 14 deletions src/backend/src/FoodDiary.Domain/Exceptions/ImportException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,4 @@ namespace FoodDiary.Domain.Exceptions;
/// <summary>
/// Represents errors that occur during diary import operations
/// </summary>
public class ImportException : Exception
{
public ImportException()
{
}

public ImportException(string message) : base(message)
{
}

public ImportException(string message, Exception innerException) : base(message, innerException)
{
}
}
public class ImportException(string message) : Exception(message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Net;
using System.Net.Http.Json;
using FoodDiary.ComponentTests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;

namespace FoodDiary.ComponentTests.Scenarios.ErrorHandling;

public class ErrorHandlingContext(FoodDiaryWebApplicationFactory factory, InfrastructureFixture infrastructure)
: BaseContext(factory, infrastructure)
{
private HttpResponseMessage? _response;
private HttpStatusCode _statusCode;

public Task Given_application_is_broken_because_of_an_unhandled_exception(Exception exception)
{
Factory = Factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new FakeExceptionActionFilter(exception));
});
});
});

return Task.CompletedTask;
}

public async Task When_user_is_trying_to_access_resource(string resource)
{
_response = await ApiClient.GetAsync(resource);
_statusCode = _response.StatusCode;
}

public Task Then_response_has_status(HttpStatusCode status)
{
_statusCode.Should().Be(status);
return Task.CompletedTask;
}

public async Task Then_response_is_problem_details()
{
var problemDetails = await _response!.Content.ReadFromJsonAsync<ProblemDetails>();
problemDetails!.Status.Should().Be((int)_statusCode);
problemDetails.Title.Should().NotBeNullOrWhiteSpace();
problemDetails.Detail.Should().NotBeNullOrWhiteSpace();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Net;
using FoodDiary.ComponentTests.Infrastructure;

namespace FoodDiary.ComponentTests.Scenarios.ErrorHandling;

public class ErrorHandlingTests(FoodDiaryWebApplicationFactory factory, InfrastructureFixture infrastructure) :
ScenarioBase<ErrorHandlingContext>(factory, infrastructure)
{
protected override ErrorHandlingContext CreateContext(
FoodDiaryWebApplicationFactory factory,
InfrastructureFixture infrastructure) => new(factory, infrastructure);

[Scenario]
public Task I_receive_unhandled_errors_in_problem_details_format()
{
var exception = new Exception("some error");

return Run(
c => c.Given_application_is_broken_because_of_an_unhandled_exception(exception),
c => c.When_user_is_trying_to_access_resource("/api/v1/auth/status"),
c => c.Then_response_has_status(HttpStatusCode.InternalServerError),
c => c.Then_response_is_problem_details());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc.Filters;

namespace FoodDiary.ComponentTests.Scenarios.ErrorHandling;

public class FakeExceptionActionFilter(Exception exception) : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
throw exception;
}

public void OnActionExecuted(ActionExecutedContext context)
{
}
}

0 comments on commit b8245f8

Please sign in to comment.