Skip to content

Commit d0972c7

Browse files
authored
Merge pull request #48 from futurum-dev/feature/improvements-to-errors-with-ProblemDetails
Improvements to errors with ProblemDetails
2 parents cb71b7b + f14bbbd commit d0972c7

12 files changed

+114
-39
lines changed

src/Futurum.WebApiEndpoint/Internal/WebApiEndpointExecutorService.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,15 @@ public static async Task ExecuteAsync(HttpContext httpContext, MetadataDefinitio
2121
}
2222
catch (Exception exception)
2323
{
24-
var failedStatusCode = (int)HttpStatusCode.InternalServerError;
25-
26-
var errorData = new IWebApiEndpointLogger.WebApiRouteErrorData(routePath, httpContext.Request.Path, "Internal Server Error", failedStatusCode, exception.Message);
24+
var errorData = new IWebApiEndpointLogger.WebApiRouteErrorData(routePath, httpContext.Request.Path, "Internal Server Error", (int)HttpStatusCode.InternalServerError, exception.Message);
2725

2826
var webApiEndpointLogger = httpContext.RequestServices.GetService<IWebApiEndpointLogger>();
2927
webApiEndpointLogger.Error(exception, errorData);
3028

31-
var errorResponse = exception.ToResultError("WebApiEndpoint - Internal Server Error")
32-
.ToProblemDetails(failedStatusCode, httpContext.Request.Path);
29+
var errorResponse = HttpStatusCode.InternalServerError.ToResultError(exception.ToResultError())
30+
.ToProblemDetails((int)HttpStatusCode.InternalServerError, httpContext.Request.Path);
3331

34-
httpContext.Response.StatusCode = failedStatusCode;
32+
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
3533

3634
httpContext.Response.ContentType = WebApiEndpointContentType.ProblemJson;
3735

src/Futurum.WebApiEndpoint/ResultErrorProblemDetailsExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ private static ProblemDetails FluentValidationResultError(FluentValidationResult
3434
private static ProblemDetails WebApiEndpointResultError(WebApiEndpointResultError webApiEndpointResultError, string requestPath) =>
3535
new()
3636
{
37-
Detail = webApiEndpointResultError.DetailErrorMessage,
37+
Detail = webApiEndpointResultError.GetChildrenErrorString(";"),
3838
Instance = requestPath,
3939
Status = (int)webApiEndpointResultError.HttpStatusCode,
40-
Title = ReasonPhrases.GetReasonPhrase((int)webApiEndpointResultError.HttpStatusCode)
40+
Title = webApiEndpointResultError.Parent.Switch(parent => parent.ToErrorString(), () => "Unknown error")
4141
};
4242

4343
private static ProblemDetails GeneralError(IResultError resultError, int failedStatusCode, string requestPath) =>
@@ -46,6 +46,6 @@ private static ProblemDetails GeneralError(IResultError resultError, int failedS
4646
Detail = resultError.ToErrorString(),
4747
Instance = requestPath,
4848
Status = failedStatusCode,
49-
Title = resultError.ToErrorStructure().Message,
49+
Title = ReasonPhrases.GetReasonPhrase(failedStatusCode),
5050
};
5151
}
Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,68 @@
11
using System.Net;
22

3+
using Futurum.Core.Linq;
4+
using Futurum.Core.Option;
35
using Futurum.Core.Result;
46

57
using Microsoft.AspNetCore.WebUtilities;
68

79
namespace Futurum.WebApiEndpoint;
810

9-
public class WebApiEndpointResultError : IResultErrorNonComposite
11+
public class WebApiEndpointResultError : IResultErrorComposite
1012
{
1113
public WebApiEndpointResultError(HttpStatusCode httpStatusCode, string detailErrorMessage)
1214
{
1315
HttpStatusCode = httpStatusCode;
14-
DetailErrorMessage = detailErrorMessage;
16+
Parent = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode).ToResultError().ToOption();
17+
Children = EnumerableExtensions.Return(detailErrorMessage.ToResultError());
1518
}
1619

20+
public WebApiEndpointResultError(HttpStatusCode httpStatusCode, IResultError error)
21+
{
22+
HttpStatusCode = httpStatusCode;
23+
Parent = ReasonPhrases.GetReasonPhrase((int)HttpStatusCode).ToResultError().ToOption();
24+
Children = EnumerableExtensions.Return(error);
25+
}
26+
27+
/// <inheritdoc />
28+
public Option<IResultErrorNonComposite> Parent { get; }
29+
30+
/// <inheritdoc />
31+
public IEnumerable<IResultError> Children { get; }
32+
1733
public HttpStatusCode HttpStatusCode { get; }
18-
public string DetailErrorMessage { get; }
1934

20-
public string GetErrorString() =>
21-
$"{ReasonPhrases.GetReasonPhrase((int)HttpStatusCode)} - {DetailErrorMessage}";
35+
/// <inheritdoc />
36+
public string GetErrorString(string seperator)
37+
{
38+
string Transform(IResultError resultError) =>
39+
resultError.ToErrorString(seperator);
40+
41+
string GetChildrenErrorString() =>
42+
Children.Select(Transform)
43+
.StringJoin(seperator);
44+
45+
string GetParentErrorString(IResultErrorNonComposite parent) =>
46+
parent.GetErrorString();
47+
48+
return Parent.Switch(parent => $"{GetParentErrorString(parent)}{seperator}{GetChildrenErrorString()}",
49+
GetChildrenErrorString);
50+
}
51+
52+
public string GetChildrenErrorString(string seperator)
53+
{
54+
string Transform(IResultError resultError) =>
55+
resultError.ToErrorString(seperator);
56+
57+
string GetChildrenErrorString() =>
58+
Children.Select(Transform)
59+
.StringJoin(seperator);
60+
61+
return GetChildrenErrorString();
62+
}
2263

23-
public ResultErrorStructure GetErrorStructure() =>
24-
new($"{ReasonPhrases.GetReasonPhrase((int)HttpStatusCode)} - {DetailErrorMessage}", Enumerable.Empty<ResultErrorStructure>());
64+
/// <inheritdoc />
65+
public ResultErrorStructure GetErrorStructure() =>
66+
Parent.Switch(parent => ResultErrorStructureExtensions.ToResultErrorStructure(parent.GetErrorString(), Children.Select(ResultErrorStructureExtensions.ToErrorStructure)),
67+
() => ResultErrorStructureExtensions.ToResultErrorStructure(Children.Select(ResultErrorStructureExtensions.ToErrorStructure)));
2568
}

src/Futurum.WebApiEndpoint/WebApiEndpointResultErrorExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ namespace Futurum.WebApiEndpoint;
77
public static class WebApiEndpointResultErrorExtensions
88
{
99
public static IResultError ToResultError(this HttpStatusCode httpStatusCode) =>
10-
new WebApiEndpointResultError(httpStatusCode, string.Empty);
10+
new WebApiEndpointResultError(httpStatusCode, ResultErrorEmpty.Value);
1111

1212
public static IResultError ToResultError(this HttpStatusCode httpStatusCode, string detailErrorMessage) =>
1313
new WebApiEndpointResultError(httpStatusCode, detailErrorMessage);
14+
15+
public static IResultError ToResultError(this HttpStatusCode httpStatusCode, IResultError error) =>
16+
new WebApiEndpointResultError(httpStatusCode, error);
1417
}

test/Futurum.WebApiEndpoint.EndToEndTests/SamplesEndToEndFeaturesErrorsTests.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
using Microsoft.AspNetCore.Mvc;
1212
using Microsoft.AspNetCore.Mvc.Testing;
13+
using Microsoft.AspNetCore.WebUtilities;
1314

1415
using Xunit;
1516

@@ -26,10 +27,12 @@ public async Task ErrorResult()
2627
var commandDto = new ErrorResultScenario.CommandDto(Guid.NewGuid().ToString());
2728
var json = JsonSerializer.Serialize(commandDto);
2829

30+
var requestPath = "/api/1.0/error-result";
31+
2932
var request = new HttpRequestMessage
3033
{
3134
Method = HttpMethod.Post,
32-
RequestUri = new Uri("/api/1.0/error-result"),
35+
RequestUri = new Uri(requestPath),
3336
Content = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json)
3437
};
3538

@@ -38,7 +41,10 @@ public async Task ErrorResult()
3841
httpResponseMessage.StatusCode.Should().Be(HttpStatusCode.BadRequest);
3942
var response = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
4043

41-
response.Title.Should().Be($"An result error has occured - {commandDto.Id}");
44+
response.Title.Should().Be(ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.BadRequest));
45+
response.Detail.Should().Be($"An result error has occured - {commandDto.Id}");
46+
response.Status.Should().Be((int)HttpStatusCode.BadRequest);
47+
response.Instance.Should().Be(requestPath);
4248
}
4349

4450
[Fact]
@@ -49,10 +55,12 @@ public async Task ErrorException()
4955
var commandDto = new ErrorExceptionScenario.CommandDto(Guid.NewGuid().ToString());
5056
var json = JsonSerializer.Serialize(commandDto);
5157

58+
var requestPath = "/api/1.0/error-exception";
59+
5260
var request = new HttpRequestMessage
5361
{
5462
Method = HttpMethod.Post,
55-
RequestUri = new Uri("/api/1.0/error-exception"),
63+
RequestUri = new Uri(requestPath),
5664
Content = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json)
5765
};
5866

@@ -61,8 +69,10 @@ public async Task ErrorException()
6169
httpResponseMessage.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
6270
var response = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
6371

64-
response.Title.Should().Be("WebApiEndpoint - Internal Server Error");
72+
response.Title.Should().Be("Internal Server Error");
6573
response.Detail.Should().Contain($"An exception has occured - {commandDto.Id}");
74+
response.Status.Should().Be((int)HttpStatusCode.InternalServerError);
75+
response.Instance.Should().Be(requestPath);
6676
}
6777

6878
private static HttpClient CreateClient() =>

test/Futurum.WebApiEndpoint.EndToEndTests/SamplesEndToEndTests.Features.Responses.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
using Microsoft.AspNetCore.Mvc;
1515
using Microsoft.AspNetCore.Mvc.Testing;
16+
using Microsoft.AspNetCore.WebUtilities;
1617

1718
using Xunit;
1819

@@ -123,7 +124,8 @@ public async Task not_set()
123124
httpResponseMessage.StatusCode.Should().Be(HttpStatusCode.NotFound);
124125
var response = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
125126

126-
response.Title.Should().Be("Unable to get range");
127+
response.Title.Should().Be(ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.NotFound));
128+
response.Detail.Should().Be("Unable to get range");
127129
}
128130

129131
[Theory]
@@ -238,7 +240,8 @@ public async Task not_set()
238240
httpResponseMessage.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable);
239241
var response = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
240242

241-
response.Title.Should()
243+
response.Title.Should().Be(ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.ServiceUnavailable));
244+
response.Detail.Should()
242245
.Be($"Failed to MapFromHeader for {typeof(WebApiEndpoint.Range).FullName} for property : '{nameof(QueryWithRequestParameterMapFromWithResponseBytesRangeScenario.RequestDto.Range)}'");
243246
}
244247

test/Futurum.WebApiEndpoint.Tests/Internal/WebApiEndpointExecutorServiceTests.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.AspNetCore.Builder;
1515
using Microsoft.AspNetCore.Http;
1616
using Microsoft.AspNetCore.Mvc;
17+
using Microsoft.AspNetCore.WebUtilities;
1718
using Microsoft.Extensions.DependencyInjection;
1819
using Microsoft.Extensions.Options;
1920

@@ -92,7 +93,9 @@ public async Task when_MetadataDefinition_is_null()
9293

9394
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(requestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
9495

95-
problemDetails.Title.Should().StartWith("WebApiEndpoint - Unable to find WebApiEndpoint for route");
96+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.BadRequest));
97+
problemDetails.Detail.Should().StartWith("WebApiEndpoint - Unable to find WebApiEndpoint for route");
98+
problemDetails.Status.Should().Be((int)HttpStatusCode.BadRequest);
9699
}
97100

98101
[Fact]
@@ -195,8 +198,9 @@ public async Task when_ApiEndpoint_call_fails()
195198

196199
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(requestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
197200

198-
problemDetails.Title.Should().Be(FailedApiEndpoint.ERROR_MESSAGE);
201+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(metadataRouteDefinition.FailedStatusCode));
199202
problemDetails.Detail.Should().Be(FailedApiEndpoint.ERROR_MESSAGE);
203+
problemDetails.Status.Should().Be(metadataRouteDefinition.FailedStatusCode);
200204
}
201205

202206
[Fact]
@@ -235,6 +239,8 @@ public async Task when_unknown_error()
235239

236240
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(requestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
237241

238-
problemDetails.Title.Should().StartWith("WebApiEndpoint - Internal Server Error");
242+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase((int)HttpStatusCode.InternalServerError));
243+
problemDetails.Detail.Should().StartWith("Failed to resolve Type");
244+
problemDetails.Status.Should().Be((int)HttpStatusCode.InternalServerError);
239245
}
240246
}

test/Futurum.WebApiEndpoint.Tests/Internal/WebApiEndpointHttpContextDispatcherTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Net;
12
using System.Text.Json;
23

34
using FluentAssertions;
@@ -11,6 +12,7 @@
1112
using Microsoft.AspNetCore.Builder;
1213
using Microsoft.AspNetCore.Http;
1314
using Microsoft.AspNetCore.Mvc;
15+
using Microsoft.AspNetCore.WebUtilities;
1416
using Microsoft.Extensions.Options;
1517

1618
using Xunit;
@@ -49,7 +51,8 @@ public async Task success()
4951

5052
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(requestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
5153

52-
problemDetails.Title.Should().Be(ErrorMessage);
53-
problemDetails.Title.Should().Be(ErrorMessage);
54+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(metadataRouteDefinition.FailedStatusCode));
55+
problemDetails.Detail.Should().Be(ErrorMessage);
56+
problemDetails.Status.Should().Be(metadataRouteDefinition.FailedStatusCode);
5457
}
5558
}

test/Futurum.WebApiEndpoint.Tests/ResultErrorProblemDetailsExtensionsTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void when_ResultErrorEmpty()
2525

2626
var problemDetails = resultError.ToProblemDetails(failedStatusCode, requestPath);
2727

28-
problemDetails.Title.Should().BeEmpty();
28+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(failedStatusCode));
2929
problemDetails.Detail.Should().BeEmpty();
3030
problemDetails.Instance.Should().Be(requestPath);
3131
problemDetails.Status.Should().Be(failedStatusCode);
@@ -42,7 +42,7 @@ public void when_ResultErrorMessage()
4242

4343
var problemDetails = resultError.ToProblemDetails(failedStatusCode, requestPath);
4444

45-
problemDetails.Title.Should().Be(errorMessage);
45+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(failedStatusCode));
4646
problemDetails.Detail.Should().Be(errorMessage);
4747
problemDetails.Instance.Should().Be(requestPath);
4848
problemDetails.Status.Should().Be(failedStatusCode);
@@ -59,7 +59,7 @@ public void when_ResultErrorException()
5959

6060
var problemDetails = resultError.ToProblemDetails(failedStatusCode, requestPath);
6161

62-
problemDetails.Title.Should().Be(errorMessage);
62+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(failedStatusCode));
6363
problemDetails.Detail.Should().Be(errorMessage);
6464
problemDetails.Instance.Should().Be(requestPath);
6565
problemDetails.Status.Should().Be(failedStatusCode);
@@ -80,7 +80,7 @@ public void when_ResultErrorComposite()
8080

8181
var problemDetails = resultError.ToProblemDetails(failedStatusCode, requestPath);
8282

83-
problemDetails.Title.Should().Be(errorMessage1);
83+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(failedStatusCode));
8484
problemDetails.Detail.Should().Be($"{errorMessage1};{errorMessage2}");
8585
problemDetails.Instance.Should().Be(requestPath);
8686
problemDetails.Status.Should().Be(failedStatusCode);

test/Futurum.WebApiEndpoint.Tests/WebApiEndpointDispatcherTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Microsoft.AspNetCore.Builder;
1616
using Microsoft.AspNetCore.Http;
1717
using Microsoft.AspNetCore.Mvc;
18+
using Microsoft.AspNetCore.WebUtilities;
1819
using Microsoft.Extensions.Options;
1920

2021
using Moq;
@@ -81,7 +82,7 @@ public async Task failure()
8182

8283
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(requestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
8384

84-
problemDetails.Title.Should().Be(ApiEndpoint.ErrorMessage);
85+
problemDetails.Title.Should().Be(ReasonPhrases.GetReasonPhrase(MetadataRouteDefinition.FailedStatusCode));
8586
problemDetails.Detail.Should().Be(ApiEndpoint.ErrorMessage);
8687

8788
mockWebApiEndpointLogger.Verify(x => x.Error(MetadataRouteDefinition.RouteTemplate, It.IsAny<IResultError>()), Times.Once);

0 commit comments

Comments
 (0)