From 2ea95c56a0617d5c377d6a339baa225d9decb1d6 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Dec 2023 13:38:04 +0800 Subject: [PATCH 1/8] Fix metrics duration and http.route tag with exception handling --- .../Internal/HostingApplicationDiagnostics.cs | 8 ++- .../src/Microsoft.AspNetCore.Hosting.csproj | 1 + .../ExceptionHandlerMiddlewareImpl.cs | 7 +- .../Microsoft.AspNetCore.Diagnostics.csproj | 1 + .../StatusCodePagesExtensions.cs | 6 +- .../ExceptionHandlerMiddlewareTest.cs | 66 ++++++++++++++++++- src/Shared/HttpExtensions.cs | 22 +++++++ 7 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index b14b8d60b0e9..cf190bbde341 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -150,7 +150,13 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp if (context.MetricsEnabled) { - var route = httpContext.GetEndpoint()?.Metadata.GetMetadata()?.Route; + var endpoint = httpContext.GetEndpoint(); + if (httpContext.Items.TryGetValue(HttpExtensions.ClearedEndpointKey, out var e) && e is Endpoint clearedEndpoint) + { + endpoint = clearedEndpoint; + } + + var route = endpoint?.Metadata.GetMetadata()?.Route; var customTags = context.MetricsTagsFeature?.TagsList; _metrics.RequestEnd( diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index 49974f5104af..c898aed6af58 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs index e8f40e9b3fab..d0579a121b94 100644 --- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs +++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs @@ -247,12 +247,7 @@ private static void ClearHttpContext(HttpContext context) // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset // the endpoint and route values to ensure things are re-calculated. - context.SetEndpoint(endpoint: null); - var routeValuesFeature = context.Features.Get(); - if (routeValuesFeature != null) - { - routeValuesFeature.RouteValues = null!; - } + HttpExtensions.ClearEndpoint(context); } private static Task ClearCacheHeaders(object state) diff --git a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj index c83395d96d9b..4657b64cbf7b 100644 --- a/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj +++ b/src/Middleware/Diagnostics/src/Microsoft.AspNetCore.Diagnostics.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs index 5cb8c981b280..a431f35582f8 100644 --- a/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs +++ b/src/Middleware/Diagnostics/src/StatusCodePage/StatusCodePagesExtensions.cs @@ -190,11 +190,7 @@ private static Func CreateHandler(string pathFormat, st // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset // the endpoint and route values to ensure things are re-calculated. - context.HttpContext.SetEndpoint(endpoint: null); - if (routeValuesFeature != null) - { - routeValuesFeature.RouteValues = null!; - } + HttpExtensions.ClearEndpoint(context.HttpContext); context.HttpContext.Request.Path = newPath; context.HttpContext.Request.QueryString = newQueryString; diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 31c3fa12f5df..0373fcdd9c96 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -21,10 +21,14 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using System.Net; namespace Microsoft.AspNetCore.Diagnostics; -public class ExceptionHandlerMiddlewareTest +public class ExceptionHandlerMiddlewareTest : LoggedTest { [Fact] public async Task ExceptionIsSetOnProblemDetailsContext() @@ -291,6 +295,66 @@ public async Task Metrics_ExceptionThrown_DefaultSettings_Handled_Reported() m => AssertRequestException(m, "System.InvalidOperationException", "handled", null)); } + [Fact] + public async Task Metrics_ExceptionThrown_Handled_RouteAvailable() + { + // Arrange + var builder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); + var endpoint = builder.Build(); + + var meterFactory = new TestMeterFactory(); + using var requestDurationCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); + using var requestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); + + using var host = new HostBuilder() + .ConfigureServices(s => + { + s.AddSingleton(meterFactory); + s.AddSingleton(LoggerFactory); + }) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseExceptionHandler(new ExceptionHandlerOptions + { + ExceptionHandler = (c) => Task.CompletedTask + }); + app.Run(context => + { + context.SetEndpoint(endpoint); + throw new Exception("Test exception"); + }); + + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + // Act + var response = await server.CreateClient().GetAsync("/path"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + await requestDurationCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); + + // Assert + Assert.Collection( + requestDurationCollector.GetMeasurementSnapshot(), + m => + { + Assert.True(m.Value > 0); + Assert.Equal(500, (int)m.Tags["http.response.status_code"]); + Assert.Equal("System.Exception", (string)m.Tags["error.type"]); + Assert.Equal("/path", (string)m.Tags["http.route"]); + }); + Assert.Collection(requestExceptionCollector.GetMeasurementSnapshot(), + m => AssertRequestException(m, "System.Exception", "handled")); + } + [Fact] public async Task Metrics_ExceptionThrown_Unhandled_Reported() { diff --git a/src/Shared/HttpExtensions.cs b/src/Shared/HttpExtensions.cs index 166bb53bcfaf..d37faa083b08 100644 --- a/src/Shared/HttpExtensions.cs +++ b/src/Shared/HttpExtensions.cs @@ -1,6 +1,7 @@ // 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.Http; +using Microsoft.AspNetCore.Http.Features; internal static class HttpExtensions { @@ -10,6 +11,8 @@ internal static class HttpExtensions internal static bool IsValidHttpMethodForForm(string method) => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method); + internal const string ClearedEndpointKey = "__ClearedEndpoint"; + internal static bool IsValidContentTypeForForm(string? contentType) { if (contentType == null) @@ -27,4 +30,23 @@ internal static bool IsValidContentTypeForForm(string? contentType) return contentType.Equals(UrlEncodedFormContentType, StringComparison.OrdinalIgnoreCase) || contentType.StartsWith(MultipartFormContentType, StringComparison.OrdinalIgnoreCase); } + + internal static void ClearEndpoint(HttpContext context) + { + var endpoint = context.GetEndpoint(); + if (endpoint != null) + { + context.Items[ClearedEndpointKey] = endpoint; + + // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset + // the endpoint and route values to ensure things are re-calculated. + context.SetEndpoint(endpoint: null); + } + + var routeValuesFeature = context.Features.Get(); + if (routeValuesFeature != null) + { + routeValuesFeature.RouteValues = null!; + } + } } From 53728733c86bf42e897d73bd8be185f7d5a4f629 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 8 Dec 2023 13:38:58 +0800 Subject: [PATCH 2/8] Clean up --- .../test/UnitTests/ExceptionHandlerMiddlewareTest.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 0373fcdd9c96..b3adfdd9e43f 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -11,20 +12,18 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.TestHost; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Logging; -using System.Net; namespace Microsoft.AspNetCore.Diagnostics; From 2cb6eb9db42b5854191cef0be8ce25da96eec460 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 9 Dec 2023 14:19:35 +0800 Subject: [PATCH 3/8] Update src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs --- .../Hosting/src/Internal/HostingApplicationDiagnostics.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index cf190bbde341..29640ef98401 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -151,7 +151,9 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp if (context.MetricsEnabled) { var endpoint = httpContext.GetEndpoint(); - if (httpContext.Items.TryGetValue(HttpExtensions.ClearedEndpointKey, out var e) && e is Endpoint clearedEndpoint) + // Some middleware re-executing the pipeline. Before they do this, they clear the endpoint. + // The original endpoint is stashed with a known key in HttpContext.Items. Use it as a fallback. + if (endpoint is null && httpContext.Items.TryGetValue(HttpExtensions.ClearedEndpointKey, out var e) && e is Endpoint clearedEndpoint) { endpoint = clearedEndpoint; } From 2814557f6cb2b645b4e0d6dab2a0172be6a89f96 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 9 Dec 2023 14:20:45 +0800 Subject: [PATCH 4/8] Update src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs --- .../Hosting/src/Internal/HostingApplicationDiagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 29640ef98401..757c67cf49a0 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -151,7 +151,7 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp if (context.MetricsEnabled) { var endpoint = httpContext.GetEndpoint(); - // Some middleware re-executing the pipeline. Before they do this, they clear the endpoint. + // Some middleware re-execute the middleware pipeline with the HttpContext. Before they do this, they clear state from context, such as the previously matched endpoint. // The original endpoint is stashed with a known key in HttpContext.Items. Use it as a fallback. if (endpoint is null && httpContext.Items.TryGetValue(HttpExtensions.ClearedEndpointKey, out var e) && e is Endpoint clearedEndpoint) { From c8c44a5c69d13d0f2ac7c4c111d9756c4c78e36d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 12 Dec 2023 14:42:59 +0800 Subject: [PATCH 5/8] Update --- .../Internal/HostingApplicationDiagnostics.cs | 9 +-------- src/Shared/HttpExtensions.cs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 757c67cf49a0..9b473ac08547 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -150,14 +150,7 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp if (context.MetricsEnabled) { - var endpoint = httpContext.GetEndpoint(); - // Some middleware re-execute the middleware pipeline with the HttpContext. Before they do this, they clear state from context, such as the previously matched endpoint. - // The original endpoint is stashed with a known key in HttpContext.Items. Use it as a fallback. - if (endpoint is null && httpContext.Items.TryGetValue(HttpExtensions.ClearedEndpointKey, out var e) && e is Endpoint clearedEndpoint) - { - endpoint = clearedEndpoint; - } - + var endpoint = HttpExtensions.GetOriginalEndpoint(httpContext); var route = endpoint?.Metadata.GetMetadata()?.Route; var customTags = context.MetricsTagsFeature?.TagsList; diff --git a/src/Shared/HttpExtensions.cs b/src/Shared/HttpExtensions.cs index d37faa083b08..34e40b216654 100644 --- a/src/Shared/HttpExtensions.cs +++ b/src/Shared/HttpExtensions.cs @@ -1,5 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Http; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -11,7 +12,7 @@ internal static class HttpExtensions internal static bool IsValidHttpMethodForForm(string method) => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method); - internal const string ClearedEndpointKey = "__ClearedEndpoint"; + internal const string OriginalEndpointKey = "__OriginalEndpoint"; internal static bool IsValidContentTypeForForm(string? contentType) { @@ -31,12 +32,25 @@ internal static bool IsValidContentTypeForForm(string? contentType) contentType.StartsWith(MultipartFormContentType, StringComparison.OrdinalIgnoreCase); } + internal static Endpoint? GetOriginalEndpoint(HttpContext context) + { + var endpoint = context.GetEndpoint(); + + // Some middleware re-execute the middleware pipeline with the HttpContext. Before they do this, they clear state from context, such as the previously matched endpoint. + // The original endpoint is stashed with a known key in HttpContext.Items. Use it as a fallback. + if (endpoint == null && context.Items.TryGetValue(OriginalEndpointKey, out var e) && e is Endpoint originalEndpoint) + { + endpoint = originalEndpoint; + } + return endpoint; + } + internal static void ClearEndpoint(HttpContext context) { var endpoint = context.GetEndpoint(); if (endpoint != null) { - context.Items[ClearedEndpointKey] = endpoint; + context.Items[OriginalEndpointKey] = endpoint; // An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset // the endpoint and route values to ensure things are re-calculated. From 688f1ac3ef130c32ff832811ce3c50315a98018f Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 12 Dec 2023 14:48:50 +0800 Subject: [PATCH 6/8] Test --- .../ExceptionHandlerMiddlewareTest.cs | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index b3adfdd9e43f..b5513d9cf0e1 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -295,11 +295,11 @@ public async Task Metrics_ExceptionThrown_DefaultSettings_Handled_Reported() } [Fact] - public async Task Metrics_ExceptionThrown_Handled_RouteAvailable() + public async Task Metrics_ExceptionThrown_Handled_UseOriginalRoute() { // Arrange - var builder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); - var endpoint = builder.Build(); + var originalEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); + var originalEndpoint = originalEndpointBuilder.Build(); var meterFactory = new TestMeterFactory(); using var requestDurationCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); @@ -323,7 +323,7 @@ public async Task Metrics_ExceptionThrown_Handled_RouteAvailable() }); app.Run(context => { - context.SetEndpoint(endpoint); + context.SetEndpoint(originalEndpoint); throw new Exception("Test exception"); }); @@ -354,6 +354,73 @@ public async Task Metrics_ExceptionThrown_Handled_RouteAvailable() m => AssertRequestException(m, "System.Exception", "handled")); } + [Fact] + public async Task Metrics_ExceptionThrown_Handled_UseNewRoute() + { + // Arrange + var originalEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/path"), 0); + var originalEndpoint = originalEndpointBuilder.Build(); + + var newEndpointBuilder = new RouteEndpointBuilder(c => Task.CompletedTask, RoutePatternFactory.Parse("/new"), 0); + var newEndpoint = newEndpointBuilder.Build(); + + var meterFactory = new TestMeterFactory(); + using var requestDurationCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Hosting", "http.server.request.duration"); + using var requestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions"); + + using var host = new HostBuilder() + .ConfigureServices(s => + { + s.AddSingleton(meterFactory); + s.AddSingleton(LoggerFactory); + }) + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseExceptionHandler(new ExceptionHandlerOptions + { + ExceptionHandler = (c) => + { + c.SetEndpoint(newEndpoint); + return Task.CompletedTask; + } + }); + app.Run(context => + { + context.SetEndpoint(originalEndpoint); + throw new Exception("Test exception"); + }); + + }); + }).Build(); + + await host.StartAsync(); + + var server = host.GetTestServer(); + + // Act + var response = await server.CreateClient().GetAsync("/path"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + await requestDurationCollector.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); + + // Assert + Assert.Collection( + requestDurationCollector.GetMeasurementSnapshot(), + m => + { + Assert.True(m.Value > 0); + Assert.Equal(500, (int)m.Tags["http.response.status_code"]); + Assert.Equal("System.Exception", (string)m.Tags["error.type"]); + Assert.Equal("/new", (string)m.Tags["http.route"]); + }); + Assert.Collection(requestExceptionCollector.GetMeasurementSnapshot(), + m => AssertRequestException(m, "System.Exception", "handled")); + } + [Fact] public async Task Metrics_ExceptionThrown_Unhandled_Reported() { From ecd98ebfb0027caccbb178aa0745b8e78528ca81 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 13 Dec 2023 08:26:50 +0800 Subject: [PATCH 7/8] PR feedback --- src/Shared/HttpExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/HttpExtensions.cs b/src/Shared/HttpExtensions.cs index 34e40b216654..1dd7b8b78918 100644 --- a/src/Shared/HttpExtensions.cs +++ b/src/Shared/HttpExtensions.cs @@ -1,6 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; From a166bf42d578cb4ff5906badafdb7c77fc610d7e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 13 Dec 2023 14:39:45 +0800 Subject: [PATCH 8/8] Update src/Shared/HttpExtensions.cs --- src/Shared/HttpExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Shared/HttpExtensions.cs b/src/Shared/HttpExtensions.cs index 1dd7b8b78918..4a0d50de25fd 100644 --- a/src/Shared/HttpExtensions.cs +++ b/src/Shared/HttpExtensions.cs @@ -12,6 +12,7 @@ internal static class HttpExtensions internal static bool IsValidHttpMethodForForm(string method) => HttpMethods.IsPost(method) || HttpMethods.IsPut(method) || HttpMethods.IsPatch(method); + // Key is a string so shared code works across different assemblies (hosting, error handling middleware, etc). internal const string OriginalEndpointKey = "__OriginalEndpoint"; internal static bool IsValidContentTypeForForm(string? contentType)