Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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);

Expand Down Expand Up @@ -96,12 +96,12 @@ public async Task TryHandleAsync_UnknownProductionException_DoesNotExposeRawExce
}

/// <summary>
/// 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.
/// </summary>
[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(
Expand All @@ -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);
Expand All @@ -128,6 +128,39 @@ public async Task TryHandleAsync_ProductionClientException_DoesNotExposeRawExcep
StringComparison.Ordinal);
}

/// <summary>
/// Verifies that production ArgumentException responses are treated as server errors and hide raw details.
/// </summary>
[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);
}

/// <summary>
/// Verifies that development responses include the exception message for diagnostics.
/// </summary>
Expand Down
Loading