diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs
index f68a2777092d..9331d504041f 100644
--- a/src/Http/Http/src/Builder/ApplicationBuilder.cs
+++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs
@@ -3,14 +3,16 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Builder;
///
/// Default implementation for .
///
-public class ApplicationBuilder : IApplicationBuilder
+public partial class ApplicationBuilder : IApplicationBuilder
{
private const string ServerFeaturesKey = "server.Features";
private const string ApplicationServicesKey = "application.Services";
@@ -117,6 +119,9 @@ public IApplicationBuilder New()
/// The .
public RequestDelegate Build()
{
+ var loggerFactory = ApplicationServices?.GetService();
+ var logger = loggerFactory?.CreateLogger();
+
RequestDelegate app = context =>
{
// If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
@@ -139,6 +144,18 @@ public RequestDelegate Build()
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
}
+
+ if (logger != null && logger.IsEnabled(LogLevel.Information))
+ {
+ Log.RequestPipelineEnd(logger,
+ context.Request.Method,
+ context.Request.Scheme,
+ context.Request.Host.Value,
+ context.Request.PathBase.Value,
+ context.Request.Path.Value,
+ context.Response.StatusCode);
+ }
+
return Task.CompletedTask;
};
@@ -149,4 +166,12 @@ public RequestDelegate Build()
return app;
}
+
+ private static partial class Log
+ {
+ [LoggerMessage(1, LogLevel.Information,
+ "Request reached the end of the middleware pipeline without being handled by application code. Request path: {Method} {Scheme}://{Host}{PathBase}{Path}, Response status code: {StatusCode}",
+ SkipEnabledCheck = true)]
+ public static partial void RequestPipelineEnd(ILogger logger, string method, string scheme, string host, string? pathBase, string? path, int statusCode);
+ }
}
diff --git a/src/Http/Http/test/ApplicationBuilderTests.cs b/src/Http/Http/test/ApplicationBuilderTests.cs
index 3211e3b0344d..a661288735e9 100644
--- a/src/Http/Http/test/ApplicationBuilderTests.cs
+++ b/src/Http/Http/test/ApplicationBuilderTests.cs
@@ -3,20 +3,22 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Builder.Internal;
-public class ApplicationBuilderTests
+public class ApplicationBuilderTests : LoggedTest
{
[Fact]
- public void BuildReturnsCallableDelegate()
+ public async Task BuildReturnsCallableDelegate()
{
var builder = new ApplicationBuilder(null);
var app = builder.Build();
var httpContext = new DefaultHttpContext();
- app.Invoke(httpContext);
+ await app.Invoke(httpContext);
Assert.Equal(404, httpContext.Response.StatusCode);
}
@@ -74,6 +76,55 @@ public async Task BuildImplicitlyThrowsForMatchedEndpointAsLastStep()
Assert.False(endpointCalled);
}
+ [Fact]
+ public async Task BuildLogAtRequestPipelineEnd()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(LoggerFactory);
+ var serviceProvider = services.BuildServiceProvider();
+
+ var builder = new ApplicationBuilder(serviceProvider);
+ var app = builder.Build();
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Protocol = "HTTP/2";
+ httpContext.Request.Scheme = "https";
+ httpContext.Request.Method = "GET";
+ httpContext.Request.Host = new HostString("localhost:5000");
+ httpContext.Request.Path = "/path";
+ httpContext.Request.PathBase = "/pathbase";
+ httpContext.Request.QueryString = new QueryString("?query=true");
+
+ await app.Invoke(httpContext);
+
+ Assert.Equal(404, httpContext.Response.StatusCode);
+
+ var log = TestSink.Writes.Single(w => w.EventId.Name == "RequestPipelineEnd");
+ Assert.Equal("Request reached the end of the middleware pipeline without being handled by application code. Request path: GET https://localhost:5000/pathbase/path, Response status code: 404", log.Message);
+ }
+
+ [Fact]
+ public async Task BuildDoesNotLogOrChangeStatusWithTerminalMiddleware()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton(LoggerFactory);
+ var serviceProvider = services.BuildServiceProvider();
+
+ var builder = new ApplicationBuilder(serviceProvider);
+ builder.Use((HttpContext context, RequestDelegate next) =>
+ {
+ context.Response.StatusCode = StatusCodes.Status204NoContent;
+ return Task.CompletedTask;
+ });
+ var app = builder.Build();
+
+ var httpContext = new DefaultHttpContext();
+ await app.Invoke(httpContext);
+
+ Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode);
+ Assert.DoesNotContain(TestSink.Writes, w => w.EventId.Name == "RequestPipelineEnd");
+ }
+
[Fact]
public void BuildDoesNotCallMatchedEndpointWhenTerminated()
{