Skip to content

Commit 3d5204e

Browse files
JamesNKBrennanConroy
authored andcommitted
Log if request reaches the end of request pipeline (dotnet#47681)
Co-authored-by: Brennan <[email protected]>
1 parent 0320864 commit 3d5204e

File tree

2 files changed

+80
-4
lines changed

2 files changed

+80
-4
lines changed

src/Http/Http/src/Builder/ApplicationBuilder.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33

44
using Microsoft.AspNetCore.Http;
55
using Microsoft.AspNetCore.Http.Features;
6+
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.Internal;
8+
using Microsoft.Extensions.Logging;
79

810
namespace Microsoft.AspNetCore.Builder;
911

1012
/// <summary>
1113
/// Default implementation for <see cref="IApplicationBuilder"/>.
1214
/// </summary>
13-
public class ApplicationBuilder : IApplicationBuilder
15+
public partial class ApplicationBuilder : IApplicationBuilder
1416
{
1517
private const string ServerFeaturesKey = "server.Features";
1618
private const string ApplicationServicesKey = "application.Services";
@@ -117,6 +119,9 @@ public IApplicationBuilder New()
117119
/// <returns>The <see cref="RequestDelegate"/>.</returns>
118120
public RequestDelegate Build()
119121
{
122+
var loggerFactory = ApplicationServices?.GetService<ILoggerFactory>();
123+
var logger = loggerFactory?.CreateLogger<ApplicationBuilder>();
124+
120125
RequestDelegate app = context =>
121126
{
122127
// 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()
139144
{
140145
context.Response.StatusCode = StatusCodes.Status404NotFound;
141146
}
147+
148+
if (logger != null && logger.IsEnabled(LogLevel.Information))
149+
{
150+
Log.RequestPipelineEnd(logger,
151+
context.Request.Method,
152+
context.Request.Scheme,
153+
context.Request.Host.Value,
154+
context.Request.PathBase.Value,
155+
context.Request.Path.Value,
156+
context.Response.StatusCode);
157+
}
158+
142159
return Task.CompletedTask;
143160
};
144161

@@ -149,4 +166,12 @@ public RequestDelegate Build()
149166

150167
return app;
151168
}
169+
170+
private static partial class Log
171+
{
172+
[LoggerMessage(1, LogLevel.Information,
173+
"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}",
174+
SkipEnabledCheck = true)]
175+
public static partial void RequestPipelineEnd(ILogger logger, string method, string scheme, string host, string? pathBase, string? path, int statusCode);
176+
}
152177
}

src/Http/Http/test/ApplicationBuilderTests.cs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@
33

44
using Microsoft.AspNetCore.Http;
55
using Microsoft.AspNetCore.Http.Features;
6+
using Microsoft.AspNetCore.Testing;
7+
using Microsoft.Extensions.DependencyInjection;
68

79
namespace Microsoft.AspNetCore.Builder.Internal;
810

9-
public class ApplicationBuilderTests
11+
public class ApplicationBuilderTests : LoggedTest
1012
{
1113
[Fact]
12-
public void BuildReturnsCallableDelegate()
14+
public async Task BuildReturnsCallableDelegate()
1315
{
1416
var builder = new ApplicationBuilder(null);
1517
var app = builder.Build();
1618

1719
var httpContext = new DefaultHttpContext();
1820

19-
app.Invoke(httpContext);
21+
await app.Invoke(httpContext);
2022
Assert.Equal(404, httpContext.Response.StatusCode);
2123
}
2224

@@ -74,6 +76,55 @@ public async Task BuildImplicitlyThrowsForMatchedEndpointAsLastStep()
7476
Assert.False(endpointCalled);
7577
}
7678

79+
[Fact]
80+
public async Task BuildLogAtRequestPipelineEnd()
81+
{
82+
var services = new ServiceCollection();
83+
services.AddSingleton(LoggerFactory);
84+
var serviceProvider = services.BuildServiceProvider();
85+
86+
var builder = new ApplicationBuilder(serviceProvider);
87+
var app = builder.Build();
88+
89+
var httpContext = new DefaultHttpContext();
90+
httpContext.Request.Protocol = "HTTP/2";
91+
httpContext.Request.Scheme = "https";
92+
httpContext.Request.Method = "GET";
93+
httpContext.Request.Host = new HostString("localhost:5000");
94+
httpContext.Request.Path = "/path";
95+
httpContext.Request.PathBase = "/pathbase";
96+
httpContext.Request.QueryString = new QueryString("?query=true");
97+
98+
await app.Invoke(httpContext);
99+
100+
Assert.Equal(404, httpContext.Response.StatusCode);
101+
102+
var log = TestSink.Writes.Single(w => w.EventId.Name == "RequestPipelineEnd");
103+
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);
104+
}
105+
106+
[Fact]
107+
public async Task BuildDoesNotLogOrChangeStatusWithTerminalMiddleware()
108+
{
109+
var services = new ServiceCollection();
110+
services.AddSingleton(LoggerFactory);
111+
var serviceProvider = services.BuildServiceProvider();
112+
113+
var builder = new ApplicationBuilder(serviceProvider);
114+
builder.Use((HttpContext context, RequestDelegate next) =>
115+
{
116+
context.Response.StatusCode = StatusCodes.Status204NoContent;
117+
return Task.CompletedTask;
118+
});
119+
var app = builder.Build();
120+
121+
var httpContext = new DefaultHttpContext();
122+
await app.Invoke(httpContext);
123+
124+
Assert.Equal(StatusCodes.Status204NoContent, httpContext.Response.StatusCode);
125+
Assert.DoesNotContain(TestSink.Writes, w => w.EventId.Name == "RequestPipelineEnd");
126+
}
127+
77128
[Fact]
78129
public void BuildDoesNotCallMatchedEndpointWhenTerminated()
79130
{

0 commit comments

Comments
 (0)