Handle JsonExceptions in RequestDelegateFactory (#36589)

This commit is contained in:
Stephen Halter 2021-09-17 09:48:25 -07:00 коммит произвёл GitHub
Родитель d0465d864e
Коммит a94189e39a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 128 добавлений и 47 удалений

Просмотреть файл

@ -8,6 +8,7 @@ using System.Linq.Expressions;
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 @@ namespace Microsoft.AspNetCore.Http
private static Func<object?, HttpContext, Task> HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext)
{
if (factoryContext.JsonRequestBodyType is null)
if (factoryContext.JsonRequestBodyParameter is null)
{
if (factoryContext.ParameterBinders.Count > 0)
{
@ -533,7 +534,12 @@ namespace Microsoft.AspNetCore.Http
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 @@ namespace Microsoft.AspNetCore.Http
Log.RequestBodyIOException(httpContext, ex);
return;
}
catch (InvalidDataException ex)
catch (JsonException ex)
{
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 @@ namespace Microsoft.AspNetCore.Http
Log.RequestBodyIOException(httpContext, ex);
return;
}
catch (InvalidDataException ex)
catch (JsonException ex)
{
Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest);
Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest);
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
@ -879,11 +885,14 @@ namespace Microsoft.AspNetCore.Http
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");
@ -892,7 +901,7 @@ namespace Microsoft.AspNetCore.Http
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, DefaultAcceptsContentType));
@ -1164,7 +1173,7 @@ namespace Microsoft.AspNetCore.Http
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; }
@ -1197,7 +1206,8 @@ namespace Microsoft.AspNetCore.Http
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}"".";
@ -1207,6 +1217,7 @@ namespace Microsoft.AspNetCore.Http
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?";
@ -1218,18 +1229,19 @@ namespace Microsoft.AspNetCore.Http
[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)
{
@ -1259,16 +1271,6 @@ namespace Microsoft.AspNetCore.Http
[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)
@ -1283,8 +1285,16 @@ namespace Microsoft.AspNetCore.Http
[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);

Просмотреть файл

@ -904,7 +904,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
}
[Fact]
public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndThrowsIfThrowOnBadRequest()
public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest()
{
var invoked = false;
@ -969,7 +969,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
}
[Fact]
public async Task RequestDelegateLogsBindAsyncFailuresAndThrowsIfThrowOnBadRequest()
public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest()
{
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
var httpContext = CreateHttpContext();
@ -1031,7 +1031,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
}
[Fact]
public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndThrowsIfThrowOnBadRequest()
public async Task RequestDelegateThrowsForSingleArgBindAsyncFailuresIfThrowOnBadRequest()
{
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
var httpContext = CreateHttpContext();
@ -1084,7 +1084,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
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";
@ -1140,7 +1140,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
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";
@ -1302,7 +1302,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
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";
@ -1421,7 +1421,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests)
{
var invoked = false;
@ -1454,7 +1454,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
}
[Fact]
public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndSets400Response()
public async Task RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response()
{
var invoked = false;
@ -1463,12 +1463,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
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<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
var factoryResult = RequestDelegateFactory.Create(TestAction);
@ -1482,14 +1482,14 @@ namespace Microsoft.AspNetCore.Routing.Internal
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.Same(invalidDataException, logMessage.Exception);
Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message);
Assert.Same(jsonException, logMessage.Exception);
}
[Fact]
public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndThrowsIfThrowOnBadRequest()
public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest()
{
var invoked = false;
@ -1498,12 +1498,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
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<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
@ -1521,9 +1521,80 @@ namespace Microsoft.AspNetCore.Routing.Internal
// 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);
Assert.Same(jsonException, badHttpRequestException.InnerException);
}
[Fact]
public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response()
{
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<IHttpRequestBodyDetectionFeature>(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<JsonException>(logMessage.Exception);
}
[Fact]
public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest()
{
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<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true });
var requestDelegate = factoryResult.RequestDelegate;
var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => 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<JsonException>(badHttpRequestException.InnerException);
}
[Fact]