diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index 78b54c7c97bd..1c33ec718d82 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -1291,6 +1291,53 @@ public Action Configure(Action next) } } + [Fact] + public async Task DeveloperExceptionPageWritesBadRequestDetailsToResponseByDefaltInDevelopment() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Development }); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.MapGet("/{parameterName}", (int parameterName) => { }); + + await app.StartAsync(); + + var client = app.GetTestClient(); + + var response = await client.GetAsync("/notAnInt"); + + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("text/plain", response.Content.Headers.ContentType.MediaType); + + var responseBody = await response.Content.ReadAsStringAsync(); + Assert.Contains("parameterName", responseBody); + Assert.Contains("notAnInt", responseBody); + } + + [Fact] + public async Task NoExceptionAreThrownForBadRequestsInProduction() + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { EnvironmentName = Environments.Production }); + builder.WebHost.UseTestServer(); + await using var app = builder.Build(); + + app.MapGet("/{parameterName}", (int parameterName) => { }); + + await app.StartAsync(); + + var client = app.GetTestClient(); + + var response = await client.GetAsync("/notAnInt"); + + Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Null(response.Content.Headers.ContentType); + + var responseBody = await response.Content.ReadAsStringAsync(); + Assert.Equal(string.Empty, responseBody); + } + class PropertyFilter : IStartupFilter { public Action Configure(Action next) diff --git a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj index 93a83901bac1..c4d761ad6ac8 100644 --- a/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj +++ b/src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt index ad6cdc5b56c8..59202b3d8a30 100644 --- a/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Extensions/src/PublicAPI.Unshipped.txt @@ -163,6 +163,8 @@ Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.get Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.RouteParameterNames.init -> void Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.get -> System.IServiceProvider? Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ServiceProvider.init -> void +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ThrowOnBadRequest.get -> bool +Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions.ThrowOnBadRequest.init -> void Microsoft.AspNetCore.Mvc.ProblemDetails Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.get -> string? Microsoft.AspNetCore.Mvc.ProblemDetails.Detail.set -> void diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index bfe3779170a9..afed27b0e2e0 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -37,10 +38,10 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); - private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, sourceValue) => - Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue)); - private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo>((httpContext, parameterType, parameterName) => - Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName)); + private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) => + Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow)); + private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, source, shouldThrow) => + Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName, source, shouldThrow)); private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target"); private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue"); @@ -86,15 +87,10 @@ public static RequestDelegateResult Create(Delegate handler, RequestDelegateFact null => null, }; - var factoryContext = new FactoryContext - { - ServiceProviderIsService = options?.ServiceProvider?.GetService() - }; - - var targetableRequestDelegate = CreateTargetableRequestDelegate(handler.Method, options, factoryContext, targetExpression); + var factoryContext = CreateFactoryContext(options); + var targetableRequestDelegate = CreateTargetableRequestDelegate(handler.Method, targetExpression, factoryContext); return new RequestDelegateResult(httpContext => targetableRequestDelegate(handler.Target, httpContext), factoryContext.Metadata); - } /// @@ -118,16 +114,13 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func() - }; + var factoryContext = CreateFactoryContext(options); if (targetFactory is null) { if (methodInfo.IsStatic) { - var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, options, factoryContext, targetExpression: null); + var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression: null, factoryContext); return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata); } @@ -136,12 +129,20 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); } - private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, RequestDelegateFactoryOptions? options, FactoryContext factoryContext, Expression? targetExpression) + private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) => + new() + { + ServiceProviderIsService = options?.ServiceProvider?.GetService(), + RouteParameters = options?.RouteParameterNames?.ToList(), + ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, + }; + + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) { // Non void return type @@ -159,11 +160,6 @@ public static RequestDelegateResult Create(MethodInfo methodInfo, Func 0 ? @@ -218,17 +214,17 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext throw new InvalidOperationException($"{parameter.Name} is not a route paramter."); } - return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext); + return BindParameterFromProperty(parameter, RouteValuesExpr, routeAttribute.Name ?? parameter.Name, factoryContext, "route"); } else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryAttribue); - return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext); + return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext, "query string"); } else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute) { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.HeaderAttribue); - return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext); + return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext, "header"); } else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute) { @@ -277,12 +273,12 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext // We're in the fallback case and we have a parameter and route parameter match so don't fallback // to query string in this case factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteParameter); - return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext); + return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext, "route"); } else { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter); - return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext); + return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext, "query string"); } } @@ -573,7 +569,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, } catch (InvalidDataException ex) { - Log.RequestBodyInvalidDataException(httpContext, ex); + Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest); httpContext.Response.StatusCode = 400; return; } @@ -606,7 +602,7 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall, catch (InvalidDataException ex) { - Log.RequestBodyInvalidDataException(httpContext, ex); + Log.RequestBodyInvalidDataException(httpContext, ex, factoryContext.ThrowOnBadRequest); httpContext.Response.StatusCode = 400; return; } @@ -633,12 +629,16 @@ private static Expression BindParameterFromService(ParameterInfo parameter) : Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); } - private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext) + private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext, string source) { var isOptional = IsOptionalParameter(parameter); var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local"); + var parameterTypeNameConstant = Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)); + var parameterNameConstant = Expression.Constant(parameter.Name); + var sourceConstant = Expression.Constant(source); + if (parameter.ParameterType == typeof(string)) { if (!isOptional) @@ -657,7 +657,8 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres Expression.Block( Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name)) + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, + Expression.Constant(factoryContext.ThrowOnBadRequest)) ) ) ); @@ -696,7 +697,8 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres if (tryParseMethodCall is null) { - throw new InvalidOperationException($"No public static bool {parameter.ParameterType.Name}.TryParse(string, out {parameter.ParameterType.Name}) method found for {parameter.Name}."); + var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); + throw new InvalidOperationException($"No public static bool {typeName}.TryParse(string, out {typeName}) method found for {parameter.Name}."); } // string tempSourceString; @@ -739,13 +741,11 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres // If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot use the parameter directly. var parsedValue = isNotNullable ? argument : Expression.Variable(nonNullableParameterType, "parsedValue"); - var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name); - var parameterNameConstant = Expression.Constant(parameter.Name); - var failBlock = Expression.Block( Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), Expression.Call(LogParameterBindingFailedMethod, - HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr)); + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, + TempSourceStringExpr, Expression.Constant(factoryContext.ThrowOnBadRequest))); var tryParseCall = tryParseMethodCall(parsedValue); @@ -762,7 +762,8 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres Expression.Block( Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, parameterTypeNameConstant, parameterNameConstant) + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, + Expression.Constant(factoryContext.ThrowOnBadRequest)) ) ) ); @@ -801,14 +802,14 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres return argument; } - private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext) => - BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext); + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext, string source) => + BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext, source); private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) { var routeValue = GetValueFromProperty(RouteValuesExpr, key); var queryValue = GetValueFromProperty(QueryExpr, key); - return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext); + return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext, "route or query string"); } private static Expression BindParameterFromBindAsync(ParameterInfo parameter, FactoryContext factoryContext) @@ -831,13 +832,18 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa if (!isOptional) { + var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); var checkRequiredBodyBlock = Expression.Block( Expression.IfThen( Expression.Equal(boundValueExpr, Expression.Constant(null)), Expression.Block( Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name)) + HttpContextExpr, + Expression.Constant(typeName), + Expression.Constant(parameter.Name), + Expression.Constant($"{typeName}.BindAsync(HttpContext, ParameterInfo)"), + Expression.Constant(factoryContext.ThrowOnBadRequest)) ) ) ); @@ -884,7 +890,11 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al Expression.Block( Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name)) + HttpContextExpr, + Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)), + Expression.Constant(parameter.Name), + Expression.Constant("body"), + Expression.Constant(factoryContext.ThrowOnBadRequest)) ) ) ); @@ -1101,10 +1111,14 @@ private static async Task ExecuteResultWriteResponse(IResult? result, HttpContex private class FactoryContext { + // Options + public IServiceProviderIsService? ServiceProviderIsService { get; init; } + public List? RouteParameters { get; init; } + public bool ThrowOnBadRequest { get; init; } + + // Temporary State public Type? JsonRequestBodyType { get; set; } public bool AllowEmptyRequestBody { get; set; } - public IServiceProviderIsService? ServiceProviderIsService { get; init; } - public List? RouteParameters { get; set; } public bool UsingTempSourceString { get; set; } public List ExtraLocals { get; } = new(); @@ -1129,46 +1143,66 @@ private static class RequestDelegateFactoryConstants public const string ServiceParameter = "Services (Inferred)"; public const string BodyParameter = "Body (Inferred)"; public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)"; - } private static partial class Log { + private const string RequestBodyInvalidDataExceptionMessage = "Reading the request body failed with an InvalidDataException."; + + private const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""."; + private const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}""."; + + private const string RequiredParameterNotProvidedLogMessage = @"Required parameter ""{ParameterType} {ParameterName}"" was not provided from {Source}."; + private const string RequiredParameterNotProvidedExceptionMessage = @"Required parameter ""{0} {1}"" was not provided from {2}."; + + // This doesn't take a shouldThrow parameter because an IOException indicates an aborted request rather than a "bad" request so + // a BadHttpRequestException feels wrong. The client shouldn't be able to read the Developer Exception Page at any rate. public static void RequestBodyIOException(HttpContext httpContext, IOException exception) => RequestBodyIOException(GetLogger(httpContext), exception); [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) - => RequestBodyInvalidDataException(GetLogger(httpContext), exception); + public static void RequestBodyInvalidDataException(HttpContext httpContext, InvalidDataException exception, bool shouldThrow) + { + if (shouldThrow) + { + throw new BadHttpRequestException(RequestBodyInvalidDataExceptionMessage, exception); + } + + RequestBodyInvalidDataException(GetLogger(httpContext), exception); + } - [LoggerMessage(2, LogLevel.Debug, "Reading the request body failed with an InvalidDataException.", EventName = "RequestBodyInvalidDataException")] + [LoggerMessage(2, LogLevel.Debug, RequestBodyInvalidDataExceptionMessage, EventName = "RequestBodyInvalidDataException")] private static partial void RequestBodyInvalidDataException(ILogger logger, InvalidDataException exception); - public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue) - => ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue); + public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue, bool shouldThrow) + { + if (shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, ParameterBindingFailedExceptionMessage, parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); + } - [LoggerMessage(3, LogLevel.Debug, - @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".", - EventName = "ParamaterBindingFailed")] - private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue); + ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue); + } - public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName) - => RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName); + [LoggerMessage(3, LogLevel.Debug, ParameterBindingFailedLogMessage, EventName = "ParameterBindingFailed")] + private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue); - [LoggerMessage(4, LogLevel.Debug, - @"Required parameter ""{ParameterType} {ParameterName}"" was not provided.", - EventName = "RequiredParameterNotProvided")] - private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName); + public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName, string source, bool shouldThrow) + { + if (shouldThrow) + { + var message = string.Format(CultureInfo.InvariantCulture, RequiredParameterNotProvidedExceptionMessage, parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); + } - public static void ParameterBindingFromHttpContextFailed(HttpContext httpContext, string parameterTypeName, string parameterName) - => ParameterBindingFromHttpContextFailed(GetLogger(httpContext), parameterTypeName, parameterName); + RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName, source); + } - [LoggerMessage(5, LogLevel.Debug, - @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from HttpContext.", - EventName = "ParameterBindingFromHttpContextFailed")] - private static partial void ParameterBindingFromHttpContextFailed(ILogger logger, string parameterType, string parameterName); + [LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")] + private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source); private static ILogger GetLogger(HttpContext httpContext) { diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 6aabd425a1fd..748bd14e2ced 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -1,10 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; + namespace Microsoft.AspNetCore.Http { /// - /// Options for controlling the behavior of when created using . + /// Options for controlling the behavior of the when created using . /// public sealed class RequestDelegateFactoryOptions { @@ -17,5 +19,11 @@ public sealed class RequestDelegateFactoryOptions /// The list of route parameter names that are specified for this handler. /// public IEnumerable? RouteParameterNames { get; init; } + + /// + /// Controls whether the should throw a in addition to + /// writing a log when handling invalid requests. + /// + public bool ThrowOnBadRequest { get; init; } } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 9833eb3d555f..16c844ec762e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -4,6 +4,7 @@ #nullable enable using System.Globalization; +using System.IO.Pipelines; using System.Linq.Expressions; using System.Net; using System.Net.Sockets; @@ -88,7 +89,7 @@ void MarkAsInvoked(HttpContext httpContext) [MemberData(nameof(NoResult))] public async Task RequestDelegateInvokesAction(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; @@ -114,7 +115,7 @@ public async Task StaticMethodInfoOverloadWorksWithBasicReflection() var factoryResult = RequestDelegateFactory.Create(methodInfo!); var requestDelegate = factoryResult.RequestDelegate; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); await requestDelegate(httpContext); @@ -160,13 +161,13 @@ object GetTarget() var factoryResult = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); var requestDelegate = factoryResult.RequestDelegate; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); await requestDelegate(httpContext); Assert.Equal(1, httpContext.Items["invoked"]); - httpContext = new DefaultHttpContext(); + httpContext = CreateHttpContext(); await requestDelegate(httpContext); @@ -201,7 +202,7 @@ static void TestAction(HttpContext httpContext, [FromRoute] int value) httpContext.Items.Add("input", value); } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); var factoryResult = RequestDelegateFactory.Create(TestAction); @@ -230,7 +231,7 @@ private static void TestOptionalString(HttpContext httpContext, string value = " [Fact] public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { @@ -256,7 +257,7 @@ public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() [Fact] public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { @@ -286,7 +287,7 @@ public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() [Fact] public async Task NullRouteParametersPrefersRouteOverQueryString() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { @@ -322,7 +323,7 @@ public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() var factoryResult = RequestDelegateFactory.Create(methodInfo!); var requestDelegate = factoryResult.RequestDelegate; - var context = new DefaultHttpContext(); + var context = CreateHttpContext(); await requestDelegate(context); @@ -345,7 +346,7 @@ public void SpecifiedEmptyRouteParametersThrowIfRouteParameterDoesNotExist() [Fact] public async Task RequestDelegatePopulatesFromRouteOptionalParameter() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestOptional); var requestDelegate = factoryResult.RequestDelegate; @@ -358,7 +359,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameter() [Fact] public async Task RequestDelegatePopulatesFromNullableOptionalParameter() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestOptional); var requestDelegate = factoryResult.RequestDelegate; @@ -371,7 +372,7 @@ public async Task RequestDelegatePopulatesFromNullableOptionalParameter() [Fact] public async Task RequestDelegatePopulatesFromOptionalStringParameter() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestOptionalString); var requestDelegate = factoryResult.RequestDelegate; @@ -387,7 +388,7 @@ public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParam const string paramName = "value"; const int originalRouteParam = 47; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); @@ -412,7 +413,7 @@ void TestAction([FromRoute(Name = specifiedName)] int foo) deserializedRouteParam = foo; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); var factoryResult = RequestDelegateFactory.Create(TestAction); @@ -436,13 +437,9 @@ void TestAction([FromRoute] int foo) deserializedRouteParam = foo; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -607,13 +604,9 @@ public static async ValueTask BindAsync(HttpContext co [MemberData(nameof(TryParsableParameters))] public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues["tryParsable"] = routeValue; - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; @@ -626,16 +619,12 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR [MemberData(nameof(TryParsableParameters))] public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQueryString(Delegate action, string? routeValue, object? expectedParameterValue) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Query = new QueryCollection(new Dictionary { ["tryParsable"] = routeValue }); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; @@ -647,7 +636,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQ [Fact] public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValueBeforeQueryString() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues["tryParsable"] = "42"; @@ -671,7 +660,7 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR [Fact] public async Task RequestDelegatePrefersBindAsyncOverTryParseString() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers.Referer = "https://example.org"; @@ -690,7 +679,7 @@ public async Task RequestDelegatePrefersBindAsyncOverTryParseString() [Fact] public async Task RequestDelegatePrefersBindAsyncOverTryParseStringForNonNullableStruct() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers.Referer = "https://example.org"; @@ -712,20 +701,12 @@ public async Task RequestDelegateUsesTryParseStringoOverBindAsyncGivenExplicitAt var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord myBindAsyncRecord) => { }); - var httpContext = new DefaultHttpContext + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; + httpContext.Request.Query = new QueryCollection(new Dictionary { - Request = - { - RouteValues = - { - ["myBindAsyncRecord"] = "foo" - }, - Query = new QueryCollection(new Dictionary - { - ["myBindAsyncRecord"] = "foo" - }), - }, - }; + ["myBindAsyncRecord"] = "foo" + }); var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; var fromQueryRequestDelegate = fromQueryFactoryResult.RequestDelegate; @@ -739,16 +720,8 @@ public async Task RequestDelegateUsesTryParseStringOverBindAsyncGivenNullableStr { var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct? myBindAsyncRecord) => { }); - var httpContext = new DefaultHttpContext - { - Request = - { - RouteValues = - { - ["myBindAsyncRecord"] = "foo" - }, - }, - }; + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); @@ -757,7 +730,7 @@ public async Task RequestDelegateUsesTryParseStringOverBindAsyncGivenNullableStr [Fact] public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyCompleted() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers.Referer = "https://example.org"; @@ -797,7 +770,7 @@ void InvalidFromHeader([FromHeader] object notTryParsable) { } public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) { var ex = Assert.Throws(() => RequestDelegateFactory.Create(action)); - Assert.Equal("No public static bool Object.TryParse(string, out Object) method found for notTryParsable.", ex.Message); + Assert.Equal("No public static bool object.TryParse(string, out object) method found for notTryParsable.", ex.Message); } [Fact] @@ -819,14 +792,9 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) invoked = true; } - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.RouteValues["tryParsable"] = "invalid!"; httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; - httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -836,29 +804,59 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) Assert.False(invoked); Assert.False(httpContext.RequestAborted.IsCancellationRequested); Assert.Equal(400, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); var logs = TestSink.Writes.ToArray(); Assert.Equal(2, logs.Length); - Assert.Equal(new EventId(3, "ParamaterBindingFailed"), logs[0].EventId); + Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[0].EventId); Assert.Equal(LogLevel.Debug, logs[0].LogLevel); - Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable"" from ""invalid!"".", logs[0].Message); + Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", logs[0].Message); - Assert.Equal(new EventId(3, "ParamaterBindingFailed"), logs[1].EventId); + Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[1].EventId); Assert.Equal(LogLevel.Debug, logs[1].LogLevel); - Assert.Equal(@"Failed to bind parameter ""Int32 tryParsable2"" from ""invalid again!"".", logs[1].Message); + Assert.Equal(@"Failed to bind parameter ""int tryParsable2"" from ""invalid again!"".", logs[1].Message); } [Fact] - public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() + public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndThrowsIfThrowOnBadRequest() { - // Not supplying any headers will cause the HttpContext TryParse overload to fail. - var httpContext = new DefaultHttpContext() + var invoked = false; + + void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), - }; + invoked = true; + } + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = "invalid!"; + httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; + + 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 bind parameter ""int tryParsable"" from ""invalid!"".", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + } + + [Fact] + public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() + { + // Not supplying any headers will cause the HttpContext TryParse overload to fail. + var httpContext = CreateHttpContext(); var invoked = false; var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => @@ -879,21 +877,47 @@ public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); Assert.Equal(LogLevel.Debug, logs[0].LogLevel); - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided.", logs[0].Message); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[0].Message); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); Assert.Equal(LogLevel.Debug, logs[1].LogLevel); - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord2"" was not provided.", logs[1].Message); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord2"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[1].Message); } [Fact] - public async Task BindAsyncExceptionsThrowException() + public async Task RequestDelegateLogsBindAsyncFailuresAndThrowsIfThrowOnBadRequest() { // Not supplying any headers will cause the HttpContext TryParse overload to fail. - var httpContext = new DefaultHttpContext() + var httpContext = CreateHttpContext(); + var invoked = false; + + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), - }; + invoked = true; + }, 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(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + } + + [Fact] + public async Task BindAsyncExceptionsThrowException() + { + // Not supplying any headers will cause the HttpContext TryParse overload to fail. + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { }); @@ -911,7 +935,7 @@ public async Task BindAsyncWithBodyArgument() Name = "Write more tests!" }; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); var stream = new MemoryStream(requestBodyBytes); ; @@ -967,7 +991,7 @@ public async Task BindAsyncRunsBeforeBodyBinding() Name = "Write more tests!" }; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); var stream = new MemoryStream(requestBodyBytes); ; @@ -1033,7 +1057,7 @@ void TestAction([FromQuery] int value) [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo) }); - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Query = query; var factoryResult = RequestDelegateFactory.Create(TestAction); @@ -1057,7 +1081,7 @@ void TestAction([FromHeader(Name = customHeaderName)] int value) deserializedRouteParam = value; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); var factoryResult = RequestDelegateFactory.Create(TestAction); @@ -1111,7 +1135,7 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) Name = "Write more tests!" }; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); var stream = new MemoryStream(requestBodyBytes); ; @@ -1149,15 +1173,11 @@ public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) [MemberData(nameof(FromBodyActions))] public async Task RequestDelegateRejectsEmptyBodyGivenFromBodyParameter(Delegate action) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; httpContext.Features.Set(new RequestBodyDetectionFeature(false)); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(action); var requestDelegate = factoryResult.RequestDelegate; @@ -1176,7 +1196,7 @@ void TestAction([FromBody(AllowEmpty = true)] Todo todo) todoToBecomeNull = todo; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; @@ -1201,7 +1221,7 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) structToBeZeroed = bodyStruct; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "0"; @@ -1213,8 +1233,10 @@ void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) Assert.Equal(default, structToBeZeroed); } - [Fact] - public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugAndDoesNotAbort() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequestDelegateLogsFromBodyIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests) { var invoked = false; @@ -1224,18 +1246,14 @@ void TestAction([FromBody] Todo todo) } var ioException = new IOException(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(ioException); - httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException); httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(TestAction); + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = throwOnBadRequests }); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -1246,6 +1264,7 @@ void TestAction([FromBody] Todo todo) var logMessage = Assert.Single(TestSink.Writes); Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal("Reading the request body failed with an IOException.", logMessage.Message); Assert.Same(ioException, logMessage.Exception); } @@ -1260,17 +1279,12 @@ void TestAction([FromBody] Todo todo) } var invalidDataException = new InvalidDataException(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Request.Headers["Content-Type"] = "application/json"; httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new IOExceptionThrowingRequestBodyStream(invalidDataException); + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(invalidDataException); httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - httpContext.Features.Set(new TestHttpRequestLifetimeFeature()); - - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1280,13 +1294,53 @@ void TestAction([FromBody] Todo todo) 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, "RequestBodyInvalidDataException"), 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); } + [Fact] + public async Task RequestDelegateLogsFromBodyInvalidDataExceptionsAsDebugAndThrowsIfThrowOnBadRequest() + { + 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 ExceptionThrowingRequestBodyStream(invalidDataException); + 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("Reading the request body failed with an InvalidDataException.", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + Assert.Same(invalidDataException, badHttpRequestException.InnerException); + } + [Fact] public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() { @@ -1343,7 +1397,7 @@ void TestImpliedFromServiceBasedOnContainer(HttpContext httpContext, MyService m [MemberData(nameof(FromServiceActions))] public async Task RequestDelegateRequiresServiceForAllFromServiceParameters(Delegate action) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.RequestServices = new EmptyServiceProvider(); var factoryResult = RequestDelegateFactory.Create(action); @@ -1359,6 +1413,7 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt var myOriginalService = new MyService(); var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); serviceCollection.AddSingleton(myOriginalService); serviceCollection.AddSingleton(myOriginalService); @@ -1366,7 +1421,7 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt using var requestScoped = services.CreateScope(); - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.RequestServices = requestScoped.ServiceProvider; var factoryResult = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); @@ -1387,7 +1442,7 @@ void TestAction(HttpContext httpContext) httpContextArgument = httpContext; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1408,10 +1463,10 @@ void TestAction(CancellationToken cancellationToken) } using var cts = new CancellationTokenSource(); - var httpContext = new DefaultHttpContext - { - RequestAborted = cts.Token - }; + var httpContext = CreateHttpContext(); + // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. + httpContext.Features.Set(new HttpRequestLifetimeFeature()); + httpContext.RequestAborted = cts.Token; var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1431,10 +1486,8 @@ void TestAction(ClaimsPrincipal user) userArgument = user; } - var httpContext = new DefaultHttpContext - { - User = new ClaimsPrincipal() - }; + var httpContext = CreateHttpContext(); + httpContext.User = new ClaimsPrincipal(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1454,7 +1507,7 @@ void TestAction(HttpRequest httpRequest) httpRequestArgument = httpRequest; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1474,7 +1527,7 @@ void TestAction(HttpResponse httpResponse) httpResponseArgument = httpResponse; } - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(TestAction); var requestDelegate = factoryResult.RequestDelegate; @@ -1517,7 +1570,7 @@ public static IEnumerable ComplexResult [MemberData(nameof(ComplexResult))] public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1592,7 +1645,7 @@ public static IEnumerable CustomResults [MemberData(nameof(CustomResults))] public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1656,7 +1709,7 @@ public static IEnumerable StringResult [MemberData(nameof(StringResult))] public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNull(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1675,7 +1728,7 @@ public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNul [MemberData(nameof(StringResult))] public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); httpContext.Response.ContentType = "application/json; charset=utf-8"; var factoryResult = RequestDelegateFactory.Create(@delegate); @@ -1714,7 +1767,7 @@ public static IEnumerable IntResult [MemberData(nameof(IntResult))] public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1756,7 +1809,7 @@ public static IEnumerable BoolResult [MemberData(nameof(BoolResult))] public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1795,7 +1848,7 @@ public static IEnumerable NullResult [MemberData(nameof(NullResult))] public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(Delegate @delegate, string message) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1841,7 +1894,7 @@ public static IEnumerable NullContentResult [MemberData(nameof(NullContentResult))] public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1889,7 +1942,7 @@ public static IEnumerable QueryParamOptionalityData [MemberData(nameof(QueryParamOptionalityData))] public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate, string paramName, string? queryParam, bool isInvalid, string? expectedResponse) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1901,10 +1954,6 @@ public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate }); } - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; @@ -1918,8 +1967,8 @@ public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate var log = Assert.Single(logs); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - var expectedType = paramName == "age" ? "Int32 age" : "String name"; - Assert.Equal($@"Required parameter ""{expectedType}"" was not provided.", log.Message); + var expectedType = paramName == "age" ? "int age" : "string name"; + Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from route or query string.", log.Message); } else { @@ -1964,7 +2013,7 @@ public static IEnumerable RouteParamOptionalityData [MemberData(nameof(RouteParamOptionalityData))] public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate, string paramName, string? routeParam, bool isInvalid, string? expectedResponse) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -1973,10 +2022,6 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate httpContext.Request.RouteValues[paramName] = routeParam; } - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(@delegate, new() { RouteParameterNames = routeParam is not null ? new[] { paramName } : Array.Empty() @@ -1994,8 +2039,8 @@ public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate var log = Assert.Single(logs); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - var expectedType = paramName == "age" ? "Int32 age" : "String name"; - Assert.Equal($@"Required parameter ""{expectedType}"" was not provided.", log.Message); + var expectedType = paramName == "age" ? "int age" : "string name"; + Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from query string.", log.Message); } else { @@ -2030,7 +2075,7 @@ public static IEnumerable BodyParamOptionalityData [MemberData(nameof(BodyParamOptionalityData))] public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, bool hasBody, bool isInvalid, string? expectedResponse) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -2066,7 +2111,7 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, var log = Assert.Single(logs); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - Assert.Equal(@"Required parameter ""Todo todo"" was not provided.", log.Message); + Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message); } else { @@ -2080,11 +2125,7 @@ public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, [Fact] public async Task RequestDelegateDoesSupportBindAsyncOptionality() { - var httpContext = new DefaultHttpContext() - { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), - }; - + var httpContext = CreateHttpContext(); var invoked = false; var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord? myBindAsyncRecord) => @@ -2125,7 +2166,7 @@ public static IEnumerable ServiceParamOptionalityData [MemberData(nameof(ServiceParamOptionalityData))] public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delegate, bool hasService, bool isInvalid) { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(LoggerFactory); @@ -2178,16 +2219,11 @@ public static IEnumerable AllowEmptyData [MemberData(nameof(AllowEmptyData))] public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allowsEmptyRequest) { - var httpContext = new DefaultHttpContext(); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var httpContext = CreateHttpContext(); var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); var logs = TestSink.Writes.ToArray(); @@ -2198,7 +2234,7 @@ public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allows var log = Assert.Single(logs); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - Assert.Equal(@"Required parameter ""Todo todo"" was not provided.", log.Message); + Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message); } else { @@ -2216,7 +2252,7 @@ public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool prov { string optionalQueryParam(string name = null) => $"Hello {name}!"; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -2246,7 +2282,7 @@ public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability( { string optionalQueryParam(int age = default(int)) => $"Age: {age}"; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -2276,7 +2312,7 @@ public async Task TreatsUnknownNullabilityAsOptionalForReferenceType(bool provid { string optionalQueryParam(string age) => $"Age: {age}"; - var httpContext = new DefaultHttpContext(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -2306,10 +2342,7 @@ public async Task CanExecuteRequestDelegateWithResultsExtension() { IResult actionWithExtensionsResult(string name) => Results.Extensions.TestResult(name); - var httpContext = new DefaultHttpContext(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var httpContext = CreateHttpContext(); var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; @@ -2330,6 +2363,22 @@ public async Task CanExecuteRequestDelegateWithResultsExtension() } + private DefaultHttpContext CreateHttpContext() + { + var responseFeature = new TestHttpResponseFeature(); + + return new() + { + RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), + Features = + { + [typeof(IHttpResponseFeature)] = responseFeature, + [typeof(IHttpResponseBodyFeature)] = responseFeature, + [typeof(IHttpRequestLifetimeFeature)] = new TestHttpRequestLifetimeFeature(), + } + }; + } + private class Todo : ITodo { public int Id { get; set; } @@ -2462,11 +2511,11 @@ public Task ExecuteAsync(HttpContext httpContext) } } - private class IOExceptionThrowingRequestBodyStream : Stream + private class ExceptionThrowingRequestBodyStream : Stream { private readonly Exception _exceptionToThrow; - public IOExceptionThrowingRequestBodyStream(Exception exceptionToThrow) + public ExceptionThrowingRequestBodyStream(Exception exceptionToThrow) { _exceptionToThrow = exceptionToThrow; } @@ -2543,6 +2592,76 @@ public void Abort() } } + private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature + { + public int StatusCode { get; set; } = 200; + public string? ReasonPhrase { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + + public bool HasStarted { get; private set; } + + // Assume any access to the response Body/Stream/Writer is writing for test purposes. + public Stream Body + { + get + { + HasStarted = true; + return Stream.Null; + } + set + { + } + } + + public Stream Stream + { + get + { + HasStarted = true; + return Stream.Null; + } + } + + public PipeWriter Writer + { + get + { + HasStarted = true; + return PipeWriter.Create(Stream.Null); + } + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + HasStarted = true; + return Task.CompletedTask; + } + + public Task CompleteAsync() + { + HasStarted = true; + return Task.CompletedTask; + } + + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) + { + HasStarted = true; + return Task.CompletedTask; + } + + public void DisableBuffering() + { + } + + public void OnStarting(Func callback, object state) + { + } + + public void OnCompleted(Func callback, object state) + { + } + } + private class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature { public RequestBodyDetectionFeature(bool canHaveBody) diff --git a/src/Http/Routing/src/Builder/DelegateEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/DelegateEndpointRouteBuilderExtensions.cs index 7158dc05f8a3..699c3f67f159 100644 --- a/src/Http/Routing/src/Builder/DelegateEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/DelegateEndpointRouteBuilderExtensions.cs @@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -168,10 +170,13 @@ public static DelegateEndpointConventionBuilder Map( routeParams.Add(part.Name); } + var routeHandlerOptions = endpoints.ServiceProvider?.GetService>(); + var options = new RequestDelegateFactoryOptions { ServiceProvider = endpoints.ServiceProvider, - RouteParameterNames = routeParams + RouteParameterNames = routeParams, + ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, }; var requestDelegateResult = RequestDelegateFactory.Create(handler, options); diff --git a/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs new file mode 100644 index 000000000000..a3781ca7e512 --- /dev/null +++ b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing +{ + internal sealed class ConfigureRouteHandlerOptions : IConfigureOptions + { + private readonly IHostEnvironment _environment; + + public ConfigureRouteHandlerOptions(IHostEnvironment environment) + { + _environment = environment; + } + + public void Configure(RouteHandlerOptions options) + { + if (_environment.IsDevelopment()) + { + options.ThrowOnBadRequest = true; + } + } + } +} diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index 2c0944eec4e4..f11f13628b83 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -98,6 +98,10 @@ public static IServiceCollection AddRouting(this IServiceCollection services) // services.TryAddSingleton(); services.TryAddSingleton(); + + // Set RouteHandlerOptions.ThrowOnBadRequest in development + services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); + return services; } diff --git a/src/Http/Routing/src/PublicAPI.Unshipped.txt b/src/Http/Routing/src/PublicAPI.Unshipped.txt index a0f1d9135cb7..068b3c9e0359 100644 --- a/src/Http/Routing/src/PublicAPI.Unshipped.txt +++ b/src/Http/Routing/src/PublicAPI.Unshipped.txt @@ -14,6 +14,10 @@ Microsoft.AspNetCore.Routing.IDataTokensMetadata.DataTokens.get -> System.Collec Microsoft.AspNetCore.Routing.IRouteNameMetadata.RouteName.get -> string? Microsoft.AspNetCore.Routing.Matching.IParameterLiteralNodeMatchingPolicy Microsoft.AspNetCore.Routing.Matching.IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string! parameterName, string! literal) -> bool +Microsoft.AspNetCore.Routing.RouteHandlerOptions +Microsoft.AspNetCore.Routing.RouteHandlerOptions.RouteHandlerOptions() -> void +Microsoft.AspNetCore.Routing.RouteHandlerOptions.ThrowOnBadRequest.get -> bool +Microsoft.AspNetCore.Routing.RouteHandlerOptions.ThrowOnBadRequest.set -> void Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteName.get -> string? Microsoft.AspNetCore.Routing.RouteNameMetadata.RouteNameMetadata(string? routeName) -> void Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata diff --git a/src/Http/Routing/src/RouteHandlerOptions.cs b/src/Http/Routing/src/RouteHandlerOptions.cs new file mode 100644 index 000000000000..c7297b86fe75 --- /dev/null +++ b/src/Http/Routing/src/RouteHandlerOptions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// Options for controlling the behavior of + /// and similar methods. + /// + public sealed class RouteHandlerOptions + { + /// + /// Controls whether endpoints should throw a in addition to + /// writing a log when handling invalid requests. + /// + /// + /// Defaults to . + /// + public bool ThrowOnBadRequest { get; set; } + } +} diff --git a/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs index 4251b3edb945..1d222785a013 100644 --- a/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs @@ -3,16 +3,12 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using Xunit; namespace Microsoft.AspNetCore.Builder { @@ -57,7 +53,7 @@ void MapDelete(IEndpointRouteBuilder routes, string template, Delegate action) = [Fact] public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); [HttpMethod("ATTRIBUTE")] void TestAction() @@ -85,7 +81,7 @@ void TestAction() [Fact] public void MapGet_BuildsEndpointWithCorrectMethod() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapGet("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -105,7 +101,7 @@ public void MapGet_BuildsEndpointWithCorrectMethod() [Fact] public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBinding() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapGet("/{id}", (int? id, HttpContext httpContext) => { if (id is not null) @@ -143,7 +139,7 @@ public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBindin [Fact] public async Task MapGetWithoutRouteParameter_BuildsEndpointWithQuerySpecificBinding() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapGet("/", (int? id, HttpContext httpContext) => { if (id is not null) @@ -184,7 +180,7 @@ public async Task MapGetWithoutRouteParameter_BuildsEndpointWithQuerySpecificBin [MemberData(nameof(MapMethods))] public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Action map, string expectedMethod) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); map(builder, "/{ID}", ([FromRoute] int? id, HttpContext httpContext) => { @@ -220,7 +216,7 @@ public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Action map, string expectedMethod) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); map(builder, "/{ID}", (int? id, HttpContext httpContext) => { @@ -259,7 +255,7 @@ public async Task MapVerbWithRouteParameterDoesNotFallbackToQuery(Action(() => builder.MapGet("/", ([FromRoute] int id) => { })); Assert.Equal("id is not a route paramter.", ex.Message); } @@ -267,7 +263,7 @@ public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist() [Fact] public void MapPost_BuildsEndpointWithCorrectMethod() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapPost("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -287,7 +283,7 @@ public void MapPost_BuildsEndpointWithCorrectMethod() [Fact] public void MapPost_BuildsEndpointWithCorrectEndpointMetadata() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapPost("/", [TestConsumesAttribute(typeof(Todo), "application/xml")] (Todo todo) => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -307,7 +303,7 @@ public void MapPost_BuildsEndpointWithCorrectEndpointMetadata() [Fact] public void MapPut_BuildsEndpointWithCorrectMethod() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapPut("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -327,7 +323,7 @@ public void MapPut_BuildsEndpointWithCorrectMethod() [Fact] public void MapDelete_BuildsEndpointWithCorrectMethod() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapDelete("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -347,7 +343,7 @@ public void MapDelete_BuildsEndpointWithCorrectMethod() [Fact] public void MapFallback_BuildsEndpointWithLowestRouteOrder() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapFallback("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -363,7 +359,7 @@ public void MapFallback_BuildsEndpointWithLowestRouteOrder() [Fact] public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapFallback(() => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -385,7 +381,7 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() public void MapMethod_DoesNotEndpointNameForInnerMethod() { var name = "InnerGetString"; - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); string InnerGetString() => "TestString"; _ = builder.MapDelete("/", InnerGetString); @@ -405,7 +401,7 @@ public void MapMethod_DoesNotEndpointNameForInnerMethod() public void MapMethod_DoesNotEndpointNameForInnerMethodWithTarget() { var name = "InnerGetString"; - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); var testString = "TestString"; string InnerGetString() => testString; _ = builder.MapDelete("/", InnerGetString); @@ -427,7 +423,7 @@ public void MapMethod_DoesNotEndpointNameForInnerMethodWithTarget() public void MapMethod_SetsEndpointNameForMethodGroup() { var name = "GetString"; - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapDelete("/", GetString); var dataSource = GetBuilderEndpointDataSource(builder); @@ -446,7 +442,7 @@ public void MapMethod_SetsEndpointNameForMethodGroup() public void WithNameOverridesDefaultEndpointName() { var name = "SomeCustomName"; - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapDelete("/", GetString).WithName(name); var dataSource = GetBuilderEndpointDataSource(builder); @@ -467,7 +463,7 @@ public void WithNameOverridesDefaultEndpointName() [Fact] public void MapMethod_DoesNotSetEndpointNameForLambda() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapDelete("/", () => { }); var dataSource = GetBuilderEndpointDataSource(builder); @@ -481,7 +477,7 @@ public void MapMethod_DoesNotSetEndpointNameForLambda() [Fact] public void WithTags_CanSetTagsForEndpoint() { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); _ = builder.MapDelete("/", GetString).WithTags("Some", "Test", "Tags"); var dataSource = GetBuilderEndpointDataSource(builder); @@ -492,6 +488,56 @@ public void WithTags_CanSetTagsForEndpoint() Assert.Equal(new[] { "Some", "Test", "Tags" }, tagsMetadata?.Tags); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapMethod_FlowsThrowOnBadHttpRequest(bool throwOnBadRequest) + { + var serviceProvider = new EmptyServiceProvider(); + serviceProvider.RouteHandlerOptions.ThrowOnBadRequest = throwOnBadRequest; + + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); + _ = builder.Map("/{id}", (int id) => { }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); + httpContext.Request.RouteValues["id"] = "invalid!"; + + if (throwOnBadRequest) + { + var ex = await Assert.ThrowsAsync(() => endpoint.RequestDelegate!(httpContext)); + Assert.Equal(400, ex.StatusCode); + } + else + { + await endpoint.RequestDelegate!(httpContext); + Assert.Equal(400, httpContext.Response.StatusCode); + } + } + + [Fact] + public async Task MapMethod_DefaultsToNotThrowOnBadHttpRequestIfItCannotResolveRouteHandlerOptions() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + + _ = builder.Map("/{id}", (int id) => { }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); + httpContext.Request.RouteValues["id"] = "invalid!"; + + await endpoint.RequestDelegate!(httpContext); + Assert.Equal(400, httpContext.Response.StatusCode); + } + class FromRoute : Attribute, IFromRouteMetadata { public string? Name { get; set; } @@ -547,18 +593,19 @@ public HttpMethodAttribute(params string[] httpMethods) } } - private class EmptyServiceProvdier : IServiceScope, IServiceProvider, IServiceScopeFactory + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory { public IServiceProvider ServiceProvider => this; + public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions(); + public IServiceScope CreateScope() { - return new EmptyServiceProvdier(); + return this; } public void Dispose() { - } public object? GetService(Type serviceType) @@ -567,6 +614,11 @@ public void Dispose() { return this; } + else if (serviceType == typeof(IOptions)) + { + return Options.Create(RouteHandlerOptions); + } + return null; } } diff --git a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj index 7b6df8ebded4..f49b90bbad72 100644 --- a/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj +++ b/src/Http/Routing/test/UnitTests/Microsoft.AspNetCore.Routing.Tests.csproj @@ -8,8 +8,9 @@ - + + diff --git a/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs new file mode 100644 index 000000000000..3bbad848082e --- /dev/null +++ b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteHandlerOptionsTests + { + [Theory] + [InlineData("Development", true)] + [InlineData("DEVELOPMENT", true)] + [InlineData("Production", false)] + [InlineData("Custom", false)] + public void ThrowOnBadRequestIsTrueIfInDevelopmentEnvironmentFalseOtherwise(string environmentName, bool expectedThrowOnBadRequest) + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + services.AddSingleton(new HostEnvironment + { + EnvironmentName = environmentName, + }); + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>().Value; + Assert.Equal(expectedThrowOnBadRequest, options.ThrowOnBadRequest); + } + + [Fact] + public void ThrowOnBadRequestIsNotOverwrittenIfNotInDevelopmentEnvironment() + { + var services = new ServiceCollection(); + + services.Configure(options => + { + options.ThrowOnBadRequest = true; + }); + + services.AddSingleton(new HostEnvironment + { + EnvironmentName = "Production", + }); + + services.AddOptions(); + services.AddRouting(); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>().Value; + Assert.True(options.ThrowOnBadRequest); + } + + [Fact] + public void RouteHandlerOptionsFailsToResolveWithoutHostEnvironment() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + var serviceProvider = services.BuildServiceProvider(); + + Assert.Throws(() => serviceProvider.GetRequiredService>()); + } + + private class HostEnvironment : IHostEnvironment + { + public string ApplicationName { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + public string ContentRootPath { get; set; } + public string EnvironmentName { get; set; } + } + } +} diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 030ef4638577..99a773cc1279 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -106,7 +106,16 @@ public async Task Invoke(HttpContext context) try { context.Response.Clear(); - context.Response.StatusCode = 500; + + // Preserve the status code that would have been written by the server automatically when a BadHttpRequestException is thrown. + if (ex is BadHttpRequestException badHttpRequestException) + { + context.Response.StatusCode = badHttpRequestException.StatusCode; + } + else + { + context.Response.StatusCode = 500; + } await _exceptionHandler(new ErrorContext(context, ex)); @@ -136,7 +145,7 @@ private Task DisplayException(ErrorContext errorContext) // If the client does not ask for HTML just format the exception as plain text if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType))) { - httpContext.Response.ContentType = "text/plain"; + httpContext.Response.ContentType = "text/plain; charset=utf-8"; var sb = new StringBuilder(); sb.AppendLine(errorContext.Exception.ToString()); @@ -165,10 +174,7 @@ private Task DisplayCompilationException( { var model = new CompilationErrorPageModel(_options); - var errorPage = new CompilationErrorPage - { - Model = model - }; + var errorPage = new CompilationErrorPage(model); if (compilationException.CompilationFailures == null) { @@ -248,6 +254,17 @@ private Task DisplayRuntimeException(HttpContext context, Exception ex) } var request = context.Request; + var title = Resources.ErrorPageHtml_Title; + + if (ex is BadHttpRequestException badHttpRequestException) + { + var badRequestReasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(badHttpRequestException.StatusCode); + + if (!string.IsNullOrEmpty(badRequestReasonPhrase)) + { + title = badRequestReasonPhrase; + } + } var model = new ErrorPageModel { @@ -257,7 +274,8 @@ private Task DisplayRuntimeException(HttpContext context, Exception ex) Cookies = request.Cookies, Headers = request.Headers, RouteValues = request.RouteValues, - Endpoint = endpointModel + Endpoint = endpointModel, + Title = title, }; var errorPage = new ErrorPage(model); diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs index 718cd466b5f7..9f43c636e3d3 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.Designer.cs @@ -1,751 +1,860 @@ -// -#pragma warning disable 1591 -namespace Microsoft.AspNetCore.Diagnostics.RazorViews -{ - #line hidden - using System.Threading.Tasks; -#line 1 "CompilationErrorPage.cshtml" -using System; - -#line default -#line hidden -#line 2 "CompilationErrorPage.cshtml" -using System.Globalization; - -#line default -#line hidden -#line 3 "CompilationErrorPage.cshtml" -using System.Linq; - -#line default -#line hidden -#line 4 "CompilationErrorPage.cshtml" -using System.Net; - -#line default -#line hidden -#line 5 "CompilationErrorPage.cshtml" -using Microsoft.AspNetCore.Diagnostics; - -#line default -#line hidden -#line 6 "CompilationErrorPage.cshtml" -using Microsoft.AspNetCore.Diagnostics.RazorViews; - -#line default -#line hidden - internal class CompilationErrorPage : Microsoft.Extensions.RazorViews.BaseView - { - #pragma warning disable 1998 - public async override global::System.Threading.Tasks.Task ExecuteAsync() - { -#line 11 "CompilationErrorPage.cshtml" - - Response.StatusCode = 500; - Response.ContentType = "text/html; charset=utf-8"; - Response.ContentLength = null; // Clear any prior Content-Length - -#line default -#line hidden - WriteLiteral("\r\n\r\n \r\n \r\n "); -#line 20 "CompilationErrorPage.cshtml" - Write(Resources.ErrorPageHtml_Title); - -#line default -#line hidden - WriteLiteral(@" - - - -

"); -#line 224 "CompilationErrorPage.cshtml" - Write(Resources.ErrorPageHtml_CompilationException); - -#line default -#line hidden - WriteLiteral("

\r\n"); -#line 225 "CompilationErrorPage.cshtml" - - var exceptionDetailId = ""; - - -#line default -#line hidden -#line 228 "CompilationErrorPage.cshtml" - for (var i = 0; i < Model.ErrorDetails.Count; i++) - { - var errorDetail = Model.ErrorDetails[i]; - exceptionDetailId = "exceptionDetail" + i; - - -#line default -#line hidden - WriteLiteral("
\r\n"); -#line 234 "CompilationErrorPage.cshtml" - - var stackFrameCount = 0; - var frameId = ""; - var fileName = errorDetail.StackFrames.FirstOrDefault()?.File; - if (!string.IsNullOrEmpty(fileName)) - { - -#line default -#line hidden - WriteLiteral("
"); -#line 240 "CompilationErrorPage.cshtml" - Write(fileName); - -#line default -#line hidden - WriteLiteral("
\r\n"); -#line 241 "CompilationErrorPage.cshtml" - } - - -#line default -#line hidden -#line 243 "CompilationErrorPage.cshtml" - if (!string.IsNullOrEmpty(errorDetail.ErrorMessage)) - { - -#line default -#line hidden - WriteLiteral("
"); -#line 245 "CompilationErrorPage.cshtml" - Write(errorDetail.ErrorMessage); - -#line default -#line hidden - WriteLiteral("
\r\n"); -#line 246 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden - WriteLiteral("
\r\n
    \r\n"); -#line 249 "CompilationErrorPage.cshtml" - foreach (var frame in errorDetail.StackFrames) - { - stackFrameCount++; - frameId = "frame" + stackFrameCount; - - -#line default -#line hidden - WriteLiteral("
  • "); -#line 269 "CompilationErrorPage.cshtml" - Write(line); - -#line default -#line hidden - WriteLiteral("
  • \r\n"); -#line 270 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden - WriteLiteral(" \r\n"); -#line 272 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden - WriteLiteral("
\r\n"); -#line 289 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden - WriteLiteral(" \r\n"); -#line 291 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden - WriteLiteral(" \r\n
\r\n \r\n"); -#line 295 "CompilationErrorPage.cshtml" - if (!string.IsNullOrEmpty(Model.CompiledContent[i])) - { - -#line default -#line hidden - WriteLiteral("
\r\n
\r\n \r\n
\r\n
\r\n \r\n"); -#line 305 "CompilationErrorPage.cshtml" - } - -#line default -#line hidden -#line 305 "CompilationErrorPage.cshtml" - - } - -#line default -#line hidden +// +#pragma warning disable 1591 +namespace Microsoft.AspNetCore.Diagnostics.RazorViews +{ + #line hidden + using System.Threading.Tasks; +#nullable restore +#line 1 "CompilationErrorPage.cshtml" +using System; + +#line default +#line hidden +#nullable disable +#nullable restore +#line 2 "CompilationErrorPage.cshtml" +using System.Globalization; + +#line default +#line hidden +#nullable disable +#nullable restore +#line 3 "CompilationErrorPage.cshtml" +using System.Linq; + +#line default +#line hidden +#nullable disable +#nullable restore +#line 4 "CompilationErrorPage.cshtml" +using System.Net; + +#line default +#line hidden +#nullable disable +#nullable restore +#line 5 "CompilationErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics; + +#line default +#line hidden +#nullable disable +#nullable restore +#line 6 "CompilationErrorPage.cshtml" +using Microsoft.AspNetCore.Diagnostics.RazorViews; + +#line default +#line hidden +#nullable disable + internal class CompilationErrorPage : Microsoft.Extensions.RazorViews.BaseView + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { +#nullable restore +#line 16 "CompilationErrorPage.cshtml" + + Response.StatusCode = 500; + Response.ContentType = "text/html; charset=utf-8"; + Response.ContentLength = null; // Clear any prior Content-Length + +#line default +#line hidden +#nullable disable + WriteLiteral("\r\n\r\n \r\n \r\n "); +#nullable restore +#line 25 "CompilationErrorPage.cshtml" + Write(Resources.ErrorPageHtml_Title); + +#line default +#line hidden +#nullable disable + WriteLiteral(@" + + + +

"); +#nullable restore +#line 235 "CompilationErrorPage.cshtml" + Write(Resources.ErrorPageHtml_CompilationException); + +#line default +#line hidden +#nullable disable + WriteLiteral("

\r\n"); +#nullable restore +#line 236 "CompilationErrorPage.cshtml" + + var exceptionDetailId = ""; + + +#line default +#line hidden +#nullable disable +#nullable restore +#line 239 "CompilationErrorPage.cshtml" + for (var i = 0; i < Model.ErrorDetails.Count; i++) + { + var errorDetail = Model.ErrorDetails[i]; + exceptionDetailId = "exceptionDetail" + i; + + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n"); +#nullable restore +#line 245 "CompilationErrorPage.cshtml" + + var stackFrameCount = 0; + var frameId = ""; + var fileName = errorDetail.StackFrames.FirstOrDefault()?.File; + if (!string.IsNullOrEmpty(fileName)) + { + +#line default +#line hidden +#nullable disable + WriteLiteral("
"); +#nullable restore +#line 251 "CompilationErrorPage.cshtml" + Write(fileName); + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n"); +#nullable restore +#line 252 "CompilationErrorPage.cshtml" + } + + +#line default +#line hidden +#nullable disable +#nullable restore +#line 254 "CompilationErrorPage.cshtml" + if (!string.IsNullOrEmpty(errorDetail.ErrorMessage)) + { + +#line default +#line hidden +#nullable disable + WriteLiteral("
"); +#nullable restore +#line 256 "CompilationErrorPage.cshtml" + Write(errorDetail.ErrorMessage); + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n"); +#nullable restore +#line 257 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n
    \r\n"); +#nullable restore +#line 260 "CompilationErrorPage.cshtml" + foreach (var frame in errorDetail.StackFrames) + { + stackFrameCount++; + frameId = "frame" + stackFrameCount; + + +#line default +#line hidden +#nullable disable + WriteLiteral("
  • "); +#nullable restore +#line 280 "CompilationErrorPage.cshtml" + Write(line); + +#line default +#line hidden +#nullable disable + WriteLiteral("
  • \r\n"); +#nullable restore +#line 281 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable + WriteLiteral(" \r\n"); +#nullable restore +#line 283 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n"); +#nullable restore +#line 300 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable + WriteLiteral(" \r\n"); +#nullable restore +#line 302 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable + WriteLiteral(" \r\n
\r\n \r\n"); +#nullable restore +#line 306 "CompilationErrorPage.cshtml" + if (!string.IsNullOrEmpty(Model.CompiledContent[i])) + { + +#line default +#line hidden +#nullable disable + WriteLiteral("
\r\n
\r\n \r\n
\r\n
\r\n \r\n"); +#nullable restore +#line 316 "CompilationErrorPage.cshtml" + } + +#line default +#line hidden +#nullable disable +#nullable restore +#line 316 "CompilationErrorPage.cshtml" + + } + +#line default +#line hidden +#nullable disable + WriteLiteral(@" + + + +"); + } + #pragma warning restore 1998 +#nullable restore +#line 8 "CompilationErrorPage.cshtml" + + public CompilationErrorPage(CompilationErrorPageModel model) + { + Model = model; + } + + public CompilationErrorPageModel Model { get; set; } + +#line default +#line hidden +#nullable disable + } +} +#pragma warning restore 1591 diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml index 961c6bedbb2f..705f60f6f424 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/CompilationErrorPage.cshtml @@ -6,6 +6,11 @@ @using Microsoft.AspNetCore.Diagnostics.RazorViews @functions { + public CompilationErrorPage(CompilationErrorPageModel model) + { + Model = model; + } + public CompilationErrorPageModel Model { get; set; } } @{ diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs index 93e6126a1ae5..516e04096bb0 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/Views/ErrorPage.Designer.cs @@ -1,1317 +1,1589 @@ -// -#pragma warning disable 1591 -namespace Microsoft.AspNetCore.Diagnostics.RazorViews -{ - #line hidden - using System.Threading.Tasks; -#line 1 "ErrorPage.cshtml" -using System; - -#line default -#line hidden -#line 2 "ErrorPage.cshtml" -using System.Globalization; - -#line default -#line hidden -#line 3 "ErrorPage.cshtml" -using System.Linq; - -#line default -#line hidden -#line 4 "ErrorPage.cshtml" -using System.Net; - -#line default -#line hidden -#line 5 "ErrorPage.cshtml" -using System.Reflection; - -#line default -#line hidden -#line 6 "ErrorPage.cshtml" -using Microsoft.AspNetCore.Diagnostics.RazorViews; - -#line default -#line hidden -#line 7 "ErrorPage.cshtml" -using Microsoft.AspNetCore.Diagnostics; - -#line default -#line hidden - internal class ErrorPage : Microsoft.Extensions.RazorViews.BaseView - { - #pragma warning disable 1998 - public async override global::System.Threading.Tasks.Task ExecuteAsync() - { -#line 17 "ErrorPage.cshtml" - - // TODO: Response.ReasonPhrase = "Internal Server Error"; - Response.ContentType = "text/html; charset=utf-8"; - string location = string.Empty; - -#line default -#line hidden - WriteLiteral("\r\n