diff --git a/src/ProjectTemplate.Web/ErrorHandling/ProblemDetailsExceptionHandler.cs b/src/ProjectTemplate.Web/ErrorHandling/ProblemDetailsExceptionHandler.cs
index b3106a6..067fc86 100644
--- a/src/ProjectTemplate.Web/ErrorHandling/ProblemDetailsExceptionHandler.cs
+++ b/src/ProjectTemplate.Web/ErrorHandling/ProblemDetailsExceptionHandler.cs
@@ -98,9 +98,10 @@ private static int GetStatusCode(Exception exception)
return exception switch
{
BadHttpRequestException => StatusCodes.Status400BadRequest,
- ArgumentException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status403Forbidden,
TimeoutException => StatusCodes.Status503ServiceUnavailable,
+ // Plain ArgumentException is intentionally treated as an internal failure. Broad argument failures can
+ // represent server-side developer bugs; request-level failures should use explicit client-input types.
_ => StatusCodes.Status500InternalServerError
};
}
diff --git a/tests/ProjectTemplate.Web.Tests/ProblemDetailsExceptionHandlerTests.cs b/tests/ProjectTemplate.Web.Tests/ProblemDetailsExceptionHandlerTests.cs
index 7154a9d..fb945ca 100644
--- a/tests/ProjectTemplate.Web.Tests/ProblemDetailsExceptionHandlerTests.cs
+++ b/tests/ProjectTemplate.Web.Tests/ProblemDetailsExceptionHandlerTests.cs
@@ -18,7 +18,7 @@ public sealed class ProblemDetailsExceptionHandlerTests
new()
{
{ ProblemDetailsExceptionCase.BadHttpRequest, StatusCodes.Status400BadRequest, "Bad Request" },
- { ProblemDetailsExceptionCase.Argument, StatusCodes.Status400BadRequest, "Bad Request" },
+ { ProblemDetailsExceptionCase.Argument, StatusCodes.Status500InternalServerError, "Internal Server Error" },
{ ProblemDetailsExceptionCase.UnauthorizedAccess, StatusCodes.Status403Forbidden, "Forbidden" },
{ ProblemDetailsExceptionCase.Timeout, StatusCodes.Status503ServiceUnavailable, "Service Unavailable" },
{ ProblemDetailsExceptionCase.Unknown, StatusCodes.Status500InternalServerError, "Internal Server Error" }
@@ -30,9 +30,9 @@ public sealed class ProblemDetailsExceptionHandlerTests
[Theory]
[MemberData(nameof(ExceptionMappings))]
public async Task TryHandleAsync_ExceptionMappings_WriteExpectedProblemDetails(
- ProblemDetailsExceptionCase exceptionCase,
- int expectedStatusCode,
- string expectedTitle)
+ ProblemDetailsExceptionCase exceptionCase,
+ int expectedStatusCode,
+ string expectedTitle)
{
Exception exception = CreateException(exceptionCase);
@@ -96,12 +96,12 @@ public async Task TryHandleAsync_UnknownProductionException_DoesNotExposeRawExce
}
///
- /// Verifies that production client-error responses do not expose raw exception messages.
+ /// Verifies that production request-level client-error responses do not expose raw exception messages.
///
[Fact]
- public async Task TryHandleAsync_ProductionClientException_DoesNotExposeRawExceptionDetail()
+ public async Task TryHandleAsync_ProductionBadHttpRequestException_DoesNotExposeRawExceptionDetail()
{
- const string SensitiveMessage = "Sensitive validation internals leak marker.";
+ const string SensitiveMessage = "Sensitive request parser leak marker.";
CapturingProblemDetailsService problemDetailsService = new();
ProblemDetailsExceptionHandler handler = CreateHandler(
@@ -112,7 +112,7 @@ public async Task TryHandleAsync_ProductionClientException_DoesNotExposeRawExcep
bool handled = await handler.TryHandleAsync(
httpContext,
- new ArgumentException(SensitiveMessage),
+ new BadHttpRequestException(SensitiveMessage, StatusCodes.Status400BadRequest),
TestContext.Current.CancellationToken);
Assert.True(handled);
@@ -128,6 +128,39 @@ public async Task TryHandleAsync_ProductionClientException_DoesNotExposeRawExcep
StringComparison.Ordinal);
}
+ ///
+ /// Verifies that production ArgumentException responses are treated as server errors and hide raw details.
+ ///
+ [Fact]
+ public async Task TryHandleAsync_ProductionArgumentException_IsServerErrorAndDoesNotExposeRawExceptionDetail()
+ {
+ const string SensitiveMessage = "Sensitive internal argument failure marker.";
+
+ CapturingProblemDetailsService problemDetailsService = new();
+ ProblemDetailsExceptionHandler handler = CreateHandler(
+ problemDetailsService,
+ Environments.Production);
+
+ DefaultHttpContext httpContext = CreateProblemDetailsHttpContext();
+
+ bool handled = await handler.TryHandleAsync(
+ httpContext,
+ new ArgumentException(SensitiveMessage),
+ TestContext.Current.CancellationToken);
+
+ Assert.True(handled);
+ Assert.NotNull(problemDetailsService.Context);
+
+ ProblemDetails problemDetails = problemDetailsService.Context.ProblemDetails;
+
+ Assert.Equal(StatusCodes.Status500InternalServerError, problemDetails.Status);
+ Assert.Equal("Internal Server Error", problemDetails.Title);
+ Assert.Equal(
+ "An unexpected error occurred. Contact support with the request ID.",
+ problemDetails.Detail);
+ Assert.DoesNotContain(SensitiveMessage, problemDetails.Detail, StringComparison.Ordinal);
+ }
+
///
/// Verifies that development responses include the exception message for diagnostics.
///