Skip to content

Commit 4afeb5e

Browse files
committed
Add global exception handling
1 parent cb1776a commit 4afeb5e

File tree

5 files changed

+100
-79
lines changed

5 files changed

+100
-79
lines changed

src/Api/Endpoints/Endpoints.cs

Lines changed: 28 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -19,105 +19,61 @@ public static void MapEndpoints(this WebApplication app)
1919
.WithApiVersionSet(apiVersionSet);
2020

2121
group.MapGet("/longestdownwardtrend",
22-
async Task<Results<Ok<LongestDownwardTrendResponse>, NoContent,
23-
BadRequest, StatusCodeHttpResult, UnauthorizedHttpResult, ProblemHttpResult>>
22+
async Task<Results<Ok<LongestDownwardTrendResponse>, NoContent, BadRequest>>
2423
(IMarketService service, DateOnly fromDate, DateOnly toDate) =>
2524
{
26-
try
27-
{
28-
var result = await service.GetLongestDownwardTrend(fromDate, toDate).ConfigureAwait(false);
29-
30-
if (result is null)
31-
{
32-
return TypedResults.NoContent();
33-
}
25+
var result = await service.GetLongestDownwardTrend(fromDate, toDate).ConfigureAwait(false);
3426

35-
return TypedResults.Ok(new LongestDownwardTrendResponse(result.Value));
36-
}
37-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
38-
{
39-
return TypedResults.StatusCode((int)HttpStatusCode.TooManyRequests);
40-
}
41-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
42-
{
43-
return TypedResults.Problem(detail: "Query spanning over 365 days", statusCode: (int)HttpStatusCode.Unauthorized);
44-
}
45-
catch (HttpRequestException)
27+
if (result is null)
4628
{
47-
return TypedResults.Problem(statusCode: (int)HttpStatusCode.InternalServerError);
29+
return TypedResults.NoContent();
4830
}
31+
32+
return TypedResults.Ok(new LongestDownwardTrendResponse(result.Value));
4933
})
5034
.WithDescription("Get longest downward trend in days between given dates")
5135
.ProducesProblem((int)HttpStatusCode.TooManyRequests)
5236
.ProducesProblem((int)HttpStatusCode.Unauthorized)
5337
.ProducesProblem((int)HttpStatusCode.InternalServerError);
5438

5539
group.MapGet("/highestradingvolume",
56-
async Task<Results<Ok<HighestTradingVolumeResponse>, NoContent,
57-
BadRequest, StatusCodeHttpResult, UnauthorizedHttpResult, ProblemHttpResult>>
40+
async Task<Results<Ok<HighestTradingVolumeResponse>, NoContent, BadRequest>>
5841
(IMarketService service, DateOnly fromDate, DateOnly toDate) =>
5942
{
60-
try
61-
{
62-
var result = await service.GetHighestTradingVolume(fromDate, toDate).ConfigureAwait(false);
63-
if (result is null)
64-
{
65-
return TypedResults.NoContent();
66-
}
67-
return TypedResults.Ok(new HighestTradingVolumeResponse
68-
(
69-
result.Value.Date,
70-
result.Value.Volume
71-
));
72-
}
73-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
74-
{
75-
return TypedResults.StatusCode((int)HttpStatusCode.TooManyRequests);
76-
}
77-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
78-
{
79-
return TypedResults.Problem(detail: "Query spanning over 365 days", statusCode: (int)HttpStatusCode.Unauthorized);
80-
}
81-
catch (HttpRequestException)
43+
var result = await service.GetHighestTradingVolume(fromDate, toDate).ConfigureAwait(false);
44+
45+
if (result is null)
8246
{
83-
return TypedResults.Problem(statusCode: (int)HttpStatusCode.InternalServerError);
47+
return TypedResults.NoContent();
8448
}
49+
50+
return TypedResults.Ok(new HighestTradingVolumeResponse
51+
(
52+
result.Value.Date,
53+
result.Value.Volume
54+
));
8555
})
8656
.WithDescription("Get the date with the highest trading volume between given dates")
8757
.ProducesProblem((int)HttpStatusCode.TooManyRequests)
8858
.ProducesProblem((int)HttpStatusCode.Unauthorized)
8959
.ProducesProblem((int)HttpStatusCode.InternalServerError);
9060

9161
group.MapGet("/buyandsell",
92-
async Task<Results<Ok<BuyAndSellResponse>, NoContent,
93-
BadRequest, StatusCodeHttpResult, UnauthorizedHttpResult, ProblemHttpResult>>
62+
async Task<Results<Ok<BuyAndSellResponse>, NoContent, BadRequest>>
9463
(IMarketService service, DateOnly fromDate, DateOnly toDate) =>
9564
{
96-
try
97-
{
98-
var result = await service.GetBestBuyAndSellDates(fromDate, toDate).ConfigureAwait(false);
99-
if (result is null)
100-
{
101-
return TypedResults.NoContent();
102-
}
103-
return TypedResults.Ok(new BuyAndSellResponse
104-
(
105-
result.Value.BuyDate,
106-
result.Value.SellDate
107-
));
108-
}
109-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
110-
{
111-
return TypedResults.StatusCode((int)HttpStatusCode.TooManyRequests);
112-
}
113-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
114-
{
115-
return TypedResults.Problem(detail: "Query spanning over 365 days", statusCode: (int)HttpStatusCode.Unauthorized);
116-
}
117-
catch (HttpRequestException)
65+
var result = await service.GetBestBuyAndSellDates(fromDate, toDate).ConfigureAwait(false);
66+
67+
if (result is null)
11868
{
119-
return TypedResults.Problem(statusCode: (int)HttpStatusCode.InternalServerError);
69+
return TypedResults.NoContent();
12070
}
71+
72+
return TypedResults.Ok(new BuyAndSellResponse
73+
(
74+
result.Value.BuyDate,
75+
result.Value.SellDate
76+
));
12177
})
12278
.WithDescription("Get pair of dates when it is best to buy and sell between given dates")
12379
.ProducesProblem((int)HttpStatusCode.TooManyRequests)

src/Api/Setup/ApiMiddleware.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ internal static class ApiMiddleware
77
{
88
public static void ConfigureMiddleware(this WebApplication app, IWebHostEnvironment environment, IConfiguration configuration)
99
{
10-
if (environment.IsDevelopment())
11-
{
12-
app.UseDeveloperExceptionPage();
13-
}
10+
app.UseExceptionHandler();
1411

1512
app.UseRouting();
1613

src/Api/Setup/ApiServices.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ internal static class ApiServices
1515
{
1616
public static void ConfigureServices(this IServiceCollection services, IWebHostEnvironment environment)
1717
{
18+
services.AddProblemDetails();
19+
services.AddExceptionHandler<GlobalExceptionHandler>();
20+
1821
if (environment.IsProduction())
1922
{
2023
services.AddApplicationInsightsTelemetry();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Net;
2+
using Microsoft.AspNetCore.Diagnostics;
3+
using Microsoft.AspNetCore.Mvc.Infrastructure;
4+
5+
namespace Api.Setup;
6+
7+
public sealed class GlobalExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IWebHostEnvironment environment) : IExceptionHandler
8+
{
9+
public async ValueTask<bool> TryHandleAsync(
10+
HttpContext httpContext,
11+
Exception exception,
12+
CancellationToken cancellationToken)
13+
{
14+
ArgumentNullException.ThrowIfNull(httpContext);
15+
16+
var statusCode = (int)HttpStatusCode.InternalServerError;
17+
var title = "Unexpected Error";
18+
var detail = "An unhandled exception occurred.";
19+
20+
switch (exception)
21+
{
22+
case BadHttpRequestException badRequestEx:
23+
statusCode = badRequestEx.StatusCode;
24+
title = "Bad Request";
25+
detail = "Invalid request parameter format.";
26+
break;
27+
28+
case HttpRequestException httpEx when httpEx.StatusCode == HttpStatusCode.TooManyRequests:
29+
statusCode = (int)httpEx.StatusCode;
30+
title = "Too Many Requests";
31+
detail = "The external service rate-limited your request.";
32+
break;
33+
34+
case HttpRequestException httpEx when httpEx.StatusCode == HttpStatusCode.Unauthorized:
35+
statusCode = (int)httpEx.StatusCode;
36+
title = "Unauthorized";
37+
detail = "Query spanning over 365 days is not allowed.";
38+
break;
39+
}
40+
41+
var problemDetails = problemDetailsFactory.CreateProblemDetails(
42+
httpContext,
43+
statusCode,
44+
title: title,
45+
detail: detail);
46+
47+
if (environment.IsDevelopment())
48+
{
49+
problemDetails.Extensions["exception"] = new
50+
{
51+
type = exception?.GetType().FullName,
52+
message = exception?.Message,
53+
stackTrace = exception?.StackTrace,
54+
innerException = exception?.InnerException?.Message
55+
};
56+
}
57+
58+
httpContext.Response.StatusCode = statusCode;
59+
httpContext.Response.ContentType = "application/problem+json";
60+
61+
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken)
62+
.ConfigureAwait(false);
63+
64+
return true;
65+
}
66+
}

src/Services/MarketClient.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,8 @@ public class MarketClient(ILogger<MarketClient> logger, IConfiguration configura
4343
return data;
4444
}
4545

46-
var exception = new HttpRequestException("Error getting market chart data", null, response.StatusCode);
47-
_logger.LogError(exception, "Error getting market chart data. Status: {Status}", response.StatusCode);
48-
throw exception;
46+
var message = $"Error getting market chart data. Response code: {response.StatusCode}";
47+
throw new HttpRequestException(message, null, response.StatusCode);
4948
}
5049
}
5150
}

0 commit comments

Comments
 (0)