Skip to content

Commit 61ade2d

Browse files
Merge pull request #9 from intility/2-add-more-responsetypes
2 parents 686b613 + 51c2297 commit 61ade2d

6 files changed

Lines changed: 142 additions & 136 deletions

File tree

JsonApiToolkit.Tests/Filters/JsonApiExceptionFilterTests.cs

Lines changed: 0 additions & 88 deletions
This file was deleted.
Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,71 @@
11
using JsonApiToolkit.Models.Errors;
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.AspNetCore.Mvc.Filters;
4-
using Microsoft.Extensions.Hosting;
54
using Microsoft.Extensions.Logging;
65

76
namespace JsonApiToolkit.Filters;
87

98
/// <summary>
10-
/// Exception filter that transforms unhandled exceptions into JSON:API compliant error responses.
9+
/// Exception filter that transforms known and unknown exceptions into JSON:API compliant error responses.
1110
/// </summary>
12-
/// <remarks>
13-
/// <para>
14-
/// This filter ensures that all unhandled exceptions in JSON:API controllers result in properly formatted
15-
/// JSON:API error responses rather than the default ASP.NET Core error format.
16-
/// </para>
17-
/// <para>
18-
/// In development environments, the filter includes detailed exception information in the response.
19-
/// In production environments, it provides a generic error message to avoid exposing sensitive details.
20-
/// </para>
21-
/// <para>
22-
/// The filter automatically logs all exceptions using the provided ILogger instance.
23-
/// </para>
24-
/// </remarks>
25-
/// <param name="logger">Logger for recording exception details</param>
26-
/// <param name="environment">Host environment to determine the level of error detail</param>
27-
public class JsonApiExceptionFilter(
28-
ILogger<JsonApiExceptionFilter> logger,
29-
IHostEnvironment environment
30-
) : IExceptionFilter
11+
public class JsonApiExceptionFilter(ILogger<JsonApiExceptionFilter> logger) : IExceptionFilter
3112
{
3213
private readonly ILogger<JsonApiExceptionFilter> _logger = logger;
33-
private readonly IHostEnvironment _environment = environment;
3414

3515
/// <summary>
36-
/// Transforms an unhandled exception into a standardized JSON:API error response.
16+
/// Handles exceptions thrown during the execution of a controller action.
3717
/// </summary>
38-
/// <param name="context">The exception context containing the exception and controller context</param>
18+
/// <param name="context">The context of the exception.</param>
3919
/// <remarks>
40-
/// This method:
41-
/// <list type="number">
42-
/// <item>
43-
/// <description>Logs the exception using the configured logger</description>
44-
/// </item>
45-
/// <item>
46-
/// <description>Creates a JSON:API error object with a 500 status code</description>
47-
/// </item>
48-
/// <item>
49-
/// <description>Sets appropriate error detail based on the environment (detailed in development, generic in production)</description>
50-
/// </item>
51-
/// <item>
52-
/// <description>Returns the error response and marks the exception as handled</description>
53-
/// </item>
54-
/// </list>
5520
/// <para>
56-
/// The resulting error response follows the JSON:API specification for error objects.
21+
/// This method inspects the exception and determines the appropriate HTTP status code
22+
/// and error message to return in the JSON:API error response.
23+
/// </para>
24+
/// <para>
25+
/// It handles known exceptions (e.g., JsonApiBadRequestException, JsonApiNotFoundException)
26+
/// and logs unexpected exceptions (500 Internal Server Error).
5727
/// </para>
5828
/// </remarks>
5929
public void OnException(ExceptionContext context)
6030
{
61-
_logger.LogError(context.Exception, "An unhandled exception occurred");
31+
var (status, title) = context.Exception switch
32+
{
33+
JsonApiBadRequestException => (400, "Bad Request"),
34+
JsonApiNotFoundException => (404, "Not Found"),
35+
JsonApiConflictException => (409, "Conflict"),
36+
JsonApiUnauthorizedException => (401, "Unauthorized"),
37+
JsonApiForbiddenException => (403, "Forbidden"),
38+
_ => (500, "Internal Server Error"),
39+
};
40+
41+
if (status == 500)
42+
{
43+
// Log full stack trace for unexpected errors
44+
_logger.LogError(context.Exception, "An unhandled exception occurred");
45+
}
46+
else
47+
{
48+
// Log only the message for handled exceptions
49+
_logger.LogInformation(
50+
"Handled JSON:API exception: {Type} - {Message}",
51+
context.Exception.GetType().Name,
52+
context.Exception.Message
53+
);
54+
}
6255

6356
var error = new JsonApiError
6457
{
65-
Status = "500",
66-
Title = "Internal Server Error",
67-
Detail = _environment.IsDevelopment()
68-
? context.Exception.Message
69-
: "An error occurred while processing your request.",
58+
Status = status.ToString(),
59+
Title = title,
60+
Detail =
61+
status != 500
62+
? context.Exception.Message
63+
: "An error occurred while processing your request.",
7064
};
7165

7266
var response = new JsonApiErrorResponse { Errors = [error] };
7367

74-
context.Result = new ObjectResult(response) { StatusCode = 500 };
68+
context.Result = new ObjectResult(response) { StatusCode = status };
7569
context.ExceptionHandled = true;
7670
}
7771
}

JsonApiToolkit/JsonApiToolkit.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<!-- Package metadata -->
99
<PackageId>Intility.JsonApiToolkit</PackageId>
10-
<Version>0.1.0</Version>
10+
<Version>0.1.1</Version>
1111
<Authors>Intility</Authors>
1212
<Company>Intility</Company>
1313
<Description>A toolkit for implementing JSON:API specification in .NET applications</Description>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Supress warnings for this file
2+
#pragma warning disable RCS1194
3+
4+
namespace JsonApiToolkit.Models.Errors;
5+
6+
/// <summary>
7+
/// Exception representing a 400 Bad Request error.
8+
/// </summary>
9+
public class JsonApiBadRequestException(string message) : Exception(message) { }
10+
11+
/// <summary>
12+
/// Exception representing a 404 Not Found error.
13+
/// </summary>
14+
public class JsonApiNotFoundException(string message) : Exception(message) { }
15+
16+
/// <summary>
17+
/// Exception representing a 409 Conflict error.
18+
/// </summary>
19+
public class JsonApiConflictException(string message) : Exception(message) { }
20+
21+
/// <summary>
22+
/// Exception representing a 401 Unauthorized error.
23+
/// </summary>
24+
public class JsonApiUnauthorizedException(string message) : Exception(message) { }
25+
26+
/// <summary>
27+
/// Exception representing a 403 Forbidden error.
28+
/// </summary>
29+
public class JsonApiForbiddenException(string message) : Exception(message) { }
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Enhanced Error Handling in JsonApiToolkit
2+
3+
JsonApiToolkit provides a clean, consistent way to handle errors in your ASP.NET Core APIs. By throwing specific exceptions in your services or controllers, you get:
4+
5+
- **Standardized JSON:API error responses** for your clients
6+
- **Clear, minimal logging**: only unexpected errors include stack traces
7+
8+
## Supported Exceptions
9+
10+
Throw these exceptions in your code to trigger the corresponding HTTP status and error response:
11+
12+
| Exception Type | HTTP Status | Typical Use Case |
13+
|-----------------------------------|-------------|---------------------------------------|
14+
| `JsonApiBadRequestException` | 400 | Validation or malformed input |
15+
| `JsonApiNotFoundException` | 404 | Resource not found |
16+
| `JsonApiConflictException` | 409 | Unique constraint or conflict |
17+
| `JsonApiUnauthorizedException` | 401 | Not authenticated |
18+
| `JsonApiForbiddenException` | 403 | Not authorized |
19+
20+
Any other unhandled exception will result in a 500 Internal Server Error.
21+
22+
> [!TIP]
23+
> If you are missing an exception type for your use case, please create an issue on GitHub.
24+
25+
## How It Works
26+
27+
- Throw a specific exception (e.g., `JsonApiNotFoundException`, `JsonApiBadRequestException`) in your code when an error occurs.
28+
- The toolkit automatically converts this into the correct HTTP status code and a JSON:API error response.
29+
- Only unexpected errors (500) are logged with stack traces; handled errors (400, 404, etc.) log just the type and message.
30+
31+
## Example Usage
32+
33+
```csharp
34+
if (string.IsNullOrWhiteSpace(request.Title))
35+
throw new JsonApiBadRequestException("Todo title cannot be empty.");
36+
37+
var todo = await _dbContext.Todos.FirstOrDefaultAsync(t => t.Id == todoId)
38+
?? throw new JsonApiNotFoundException($"Todo with ID {todoId} not found.");
39+
```
40+
41+
---
42+
43+
## Example Client Response
44+
45+
If a todo is not found, the client receives:
46+
47+
```json
48+
{
49+
"errors": [
50+
{
51+
"status": "404",
52+
"title": "Not Found",
53+
"detail": "Todo with ID 42 not found."
54+
}
55+
]
56+
}
57+
```
58+
59+
---
60+
61+
## Example Console Log
62+
63+
For the same error, your log will show:
64+
65+
```
66+
[09:07:11 INF] Handled JSON:API exception: JsonApiNotFoundException - Todo with ID 42 not found.
67+
```
68+
69+
*No stack trace is logged for handled errors like 400, 404, or 409.*

docs/docs/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
href: querying.md
77
- name: Use Cases
88
href: use-cases.md
9+
- name: Enhanced Error Handling
10+
href: enhanced-error-handling.md
911
- name: API Controller Examples
1012
href: api-controller-examples.md
1113
- name: Integrations

0 commit comments

Comments
 (0)