From 696f5e14c54862091f367feb8dc81fa890cc5db4 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 15 Sep 2021 18:01:59 -0700 Subject: [PATCH 1/2] Handle JsonExceptions like InvalidDataExceptions in RequestDelegateFactory --- .../src/RequestDelegateFactory.cs | 46 ++++++---- .../test/RequestDelegateFactoryTests.cs | 83 +++++++++++++++++-- 2 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 627c38d7e64d..f825995cf483 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Security.Claims; using System.Text; +using System.Text.Json; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -504,7 +505,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) { - if (factoryContext.JsonRequestBodyType is null) + if (factoryContext.JsonRequestBodyParameter is null) { if (factoryContext.ParameterBinders.Count > 0) { @@ -533,7 +534,12 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile(); } - var bodyType = factoryContext.JsonRequestBodyType; + var bodyType = factoryContext.JsonRequestBodyParameter.ParameterType; + var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.JsonRequestBodyParameter.ParameterType, fullName: false); + var parameterName = factoryContext.JsonRequestBodyParameter.Name; + + Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); + object? defaultBodyValue = null; if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType) @@ -580,10 +586,10 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, Log.RequestBodyIOException(httpContext, ex); return; } - catch (InvalidDataException ex) + catch (Exception ex) when (ex is InvalidDataException || ex is JsonException) { - Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest); - httpContext.Response.StatusCode = 400; + Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; return; } } @@ -618,10 +624,10 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, Log.RequestBodyIOException(httpContext, ex); return; } - catch (InvalidDataException ex) + catch (Exception ex) when (ex is InvalidDataException || ex is JsonException) { - Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest); + Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; return; } @@ -878,11 +884,14 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) { - if (factoryContext.JsonRequestBodyType is not null) + if (factoryContext.JsonRequestBodyParameter is not null) { factoryContext.HasMultipleBodyParameters = true; var parameterName = parameter.Name; - if (parameterName is not null && factoryContext.TrackedParameters.ContainsKey(parameterName)) + + Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); + + if (factoryContext.TrackedParameters.ContainsKey(parameterName)) { factoryContext.TrackedParameters.Remove(parameterName); factoryContext.TrackedParameters.Add(parameterName, "UNKNOWN"); @@ -891,7 +900,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al var isOptional = IsOptionalParameter(parameter, factoryContext); - factoryContext.JsonRequestBodyType = parameter.ParameterType; + factoryContext.JsonRequestBodyParameter = parameter; factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional; factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultContentType)); @@ -1163,7 +1172,7 @@ private class FactoryContext public bool DisableInferredFromBody { get; init; } // Temporary State - public Type? JsonRequestBodyType { get; set; } + public ParameterInfo? JsonRequestBodyParameter { get; set; } public bool AllowEmptyRequestBody { get; set; } public bool UsingTempSourceString { get; set; } @@ -1196,7 +1205,8 @@ private static class RequestDelegateFactoryConstants private static partial class Log { - private const string RequestBodyInvalidDataExceptionMessage = "Reading the request body failed with an InvalidDataException."; + private const string InvalidJsonRequestBodyMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as JSON."; + private const string InvalidJsonRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as JSON."; private const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""."; private const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}""."; @@ -1206,6 +1216,7 @@ private static partial class Log private const string UnexpectedContentTypeLogMessage = @"Expected a supported JSON media type but got ""{ContentType}""."; private const string UnexpectedContentTypeExceptionMessage = @"Expected a supported JSON media type but got ""{0}""."; + private const string ImplicitBodyNotProvidedLogMessage = @"Implicit body inferred for parameter ""{ParameterName}"" but no body was provided. Did you mean to use a Service instead?"; private const string ImplicitBodyNotProvidedExceptionMessage = @"Implicit body inferred for parameter ""{0}"" but no body was provided. Did you mean to use a Service instead?"; @@ -1217,18 +1228,19 @@ public static void RequestBodyIOException(HttpContext httpContext, IOException e [LoggerMessage(1, LogLevel.Debug, "Reading the request body failed with an IOException.", EventName = "RequestBodyIOException")] private static partial void RequestBodyIOException(ILogger logger, IOException exception); - public static void RequestBodyInvalidDataException(HttpContext httpContext, InvalidDataException exception, bool shouldThrow) + public static void InvalidJsonRequestBody(HttpContext httpContext, string parameterTypeName, string parameterName, Exception exception, bool shouldThrow) { if (shouldThrow) { - throw new BadHttpRequestException(RequestBodyInvalidDataExceptionMessage, exception); + var message = string.Format(CultureInfo.InvariantCulture, InvalidJsonRequestBodyExceptionMessage, parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); } - RequestBodyInvalidDataException(GetLogger(httpContext), exception); + InvalidJsonRequestBody(GetLogger(httpContext), parameterTypeName, parameterName, exception); } - [LoggerMessage(2, LogLevel.Debug, RequestBodyInvalidDataExceptionMessage, EventName = "RequestBodyInvalidDataException")] - private static partial void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception); + [LoggerMessage(2, LogLevel.Debug, InvalidJsonRequestBodyMessage, EventName = "InvalidJsonRequestBody")] + private static partial void InvalidJsonRequestBody(ILogger logger, string parameterType, string parameterName, Exception exception); public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue, bool shouldThrow) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 16787a4c98fe..67242f6c728c 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -956,7 +956,7 @@ public async Task BindAsyncWithBodyArgument() var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); ; + var stream = new MemoryStream(requestBodyBytes); httpContext.Request.Body = stream; httpContext.Request.Headers["Content-Type"] = "application/json"; @@ -1012,7 +1012,7 @@ public async Task BindAsyncRunsBeforeBodyBinding() var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); ; + var stream = new MemoryStream(requestBodyBytes); httpContext.Request.Body = stream; httpContext.Request.Headers["Content-Type"] = "application/json"; @@ -1174,7 +1174,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); ; + var stream = new MemoryStream(requestBodyBytes); httpContext.Request.Body = stream; httpContext.Request.Headers["Content-Type"] = "application/json"; @@ -1354,12 +1354,45 @@ void TestAction([FromBody] Todo todo) Assert.False(httpContext.Response.HasStarted); var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(2, "RequestBodyInvalidDataException"), logMessage.EventId); + Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal("Reading the request body failed with an InvalidDataException.", logMessage.Message); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); Assert.Same(invalidDataException, logMessage.Exception); } + [Fact] + public async Task RequestDelegateLogsFromBodyJsonExceptionAsDebugAndSets400Response() + { + var invoked = false; + + void TestAction([FromBody] Todo todo) + { + invoked = true; + } + + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); + + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); + Assert.IsType(logMessage.Exception); + } + [Fact] public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndThrowsIfThrowOnBadRequest() { @@ -1393,11 +1426,49 @@ void TestAction([FromBody] Todo todo) // We don't log bad requests when we throw. Assert.Empty(TestSink.Writes); - Assert.Equal("Reading the request body failed with an InvalidDataException.", badHttpRequestException.Message); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); Assert.Equal(400, badHttpRequestException.StatusCode); Assert.Same(invalidDataException, badHttpRequestException.InnerException); } + [Fact] + public async Task RequestDelegateLogsFromBodyJsonExceptionsAsDebugAndThrowsIfThrowOnBadRequest() + { + var invoked = false; + + void TestAction([FromBody] Todo todo) + { + invoked = true; + } + + var invalidDataException = new InvalidDataException(); + + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; + + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + + Assert.False(invoked); + + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); + + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); + + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + Assert.IsType(badHttpRequestException.InnerException); + } + [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() { From 9c81442145d7b5e29712537c336f2d49cd744aa6 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 16 Sep 2021 13:41:42 -0700 Subject: [PATCH 2/2] Don't catch InvalidDataExceptions when reading JSON --- .../src/RequestDelegateFactory.cs | 26 ++++---- .../test/RequestDelegateFactoryTests.cs | 66 +++++++++---------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index f825995cf483..cb63f914f3d6 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -586,7 +586,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, Log.RequestBodyIOException(httpContext, ex); return; } - catch (Exception ex) when (ex is InvalidDataException || ex is JsonException) + catch (JsonException ex) { Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; @@ -624,7 +624,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, Log.RequestBodyIOException(httpContext, ex); return; } - catch (Exception ex) when (ex is InvalidDataException || ex is JsonException) + catch (JsonException ex) { Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); @@ -1270,16 +1270,6 @@ public static void RequiredParameterNotProvided(HttpContext httpContext, string [LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")] private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source); - public static void UnexpectedContentType(HttpContext httpContext, string? contentType, bool shouldThrow) - { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, UnexpectedContentTypeExceptionMessage, contentType); - throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); - } - - UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)"); - } public static void ImplicitBodyNotProvided(HttpContext httpContext, string parameterName, bool shouldThrow) { if (shouldThrow) @@ -1294,8 +1284,16 @@ public static void ImplicitBodyNotProvided(HttpContext httpContext, string param [LoggerMessage(5, LogLevel.Debug, ImplicitBodyNotProvidedLogMessage, EventName = "ImplicitBodyNotProvided")] private static partial void ImplicitBodyNotProvided(ILogger logger, string parameterName); - public static void UnexpectedContentType(HttpContext httpContext, string? contentType) - => UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)"); + public static void UnexpectedContentType(HttpContext httpContext, string? contentType, bool shouldThrow) + { + if (shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, UnexpectedContentTypeExceptionMessage, contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); + } + + UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)"); + } [LoggerMessage(6, LogLevel.Debug, UnexpectedContentTypeLogMessage, EventName = "UnexpectedContentType")] private static partial void UnexpectedContentType(ILogger logger, string contentType); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 67242f6c728c..28a1071c4429 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -838,7 +838,7 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) } [Fact] - public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndThrowsIfThrowOnBadRequest() + public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest() { var invoked = false; @@ -903,7 +903,7 @@ public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() } [Fact] - public async Task RequestDelegateLogsBindAsyncFailuresAndThrowsIfThrowOnBadRequest() + public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest() { // Not supplying any headers will cause the HttpContext TryParse overload to fail. var httpContext = CreateHttpContext(); @@ -1293,7 +1293,7 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) [Theory] [InlineData(true)] [InlineData(false)] - public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests) + public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests) { var invoked = false; @@ -1326,7 +1326,7 @@ void TestAction([FromBody] Todo todo) } [Fact] - public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndSets400Response() + public async Task RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response() { var invoked = false; @@ -1335,12 +1335,12 @@ void TestAction([FromBody] Todo todo) invoked = true; } - var invalidDataException = new InvalidDataException(); + var jsonException = new JsonException(); var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(invalidDataException); + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); httpContext.Features.Set(new RequestBodyDetectionFeature(true)); var factoryResult = RequestDelegateFactory.Create(TestAction); @@ -1357,11 +1357,11 @@ void TestAction([FromBody] Todo todo) Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); - Assert.Same(invalidDataException, logMessage.Exception); + Assert.Same(jsonException, logMessage.Exception); } [Fact] - public async Task RequestDelegateLogsFromBodyJsonExceptionAsDebugAndSets400Response() + public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest() { var invoked = false; @@ -1370,31 +1370,36 @@ void TestAction([FromBody] Todo todo) invoked = true; } + var jsonException = new JsonException(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var factoryResult = RequestDelegateFactory.Create(TestAction); + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); Assert.False(invoked); + + // The httpContext should be untouched. Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); + Assert.Equal(200, httpContext.Response.StatusCode); Assert.False(httpContext.Response.HasStarted); - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); - Assert.IsType(logMessage.Exception); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); + + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + Assert.Same(jsonException, badHttpRequestException.InnerException); } [Fact] - public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndThrowsIfThrowOnBadRequest() + public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response() { var invoked = false; @@ -1403,36 +1408,31 @@ void TestAction([FromBody] Todo todo) invoked = true; } - var invalidDataException = new InvalidDataException(); - var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(invalidDataException); + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); + var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + await requestDelegate(httpContext); Assert.False(invoked); - - // The httpContext should be untouched. Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(400, httpContext.Response.StatusCode); Assert.False(httpContext.Response.HasStarted); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); - - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - Assert.Same(invalidDataException, badHttpRequestException.InnerException); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); + Assert.IsType(logMessage.Exception); } [Fact] - public async Task RequestDelegateLogsFromBodyJsonExceptionsAsDebugAndThrowsIfThrowOnBadRequest() + public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest() { var invoked = false;