Skip to content

Commit b740b6a

Browse files
halter73JamesNK
andauthored
Keep Http2Connection's ExecutionContext in middleware (#26304)
* Add Http2EndToEndTests * Finish MiddlewareIsRunWithConnectionLoggingScopeForHttp2Requests * Keep Http2Connection's ExecutionContext in middleware * Restore EC before awaiting next request * Update src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs Co-authored-by: James Newton-King <james@newtonking.com> * Move InitialExecutionContext to HttpConnectionContext Co-authored-by: James Newton-King <james@newtonking.com>
1 parent 720f882 commit b740b6a

File tree

5 files changed

+163
-7
lines changed

5 files changed

+163
-7
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.IO.Pipelines;
99
using System.Linq;
1010
using System.Net;
11-
using System.Net.Http.Headers;
1211
using System.Runtime.CompilerServices;
1312
using System.Text;
1413
using System.Threading;
@@ -609,9 +608,21 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
609608

610609
private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application)
611610
{
612-
var cleanContext = ExecutionContext.Capture();
613611
while (_keepAlive)
614612
{
613+
if (_context.InitialExecutionContext is null)
614+
{
615+
// If this is a first request on a non-Http2Connection, capture a clean ExecutionContext.
616+
_context.InitialExecutionContext = ExecutionContext.Capture();
617+
}
618+
else
619+
{
620+
// Clear any AsyncLocals set during the request; back to a clean state ready for next request
621+
// And/or reset to Http2Connection's ExecutionContext giving access to the connection logging scope
622+
// and any other AsyncLocals set by connection middleware.
623+
ExecutionContext.Restore(_context.InitialExecutionContext);
624+
}
625+
615626
BeginRequestProcessing();
616627

617628
var result = default(ReadResult);
@@ -737,9 +748,6 @@ private async Task ProcessRequests<TContext>(IHttpApplication<TContext> applicat
737748
{
738749
await messageBody.StopAsync();
739750
}
740-
741-
// Clear any AsyncLocals set during the request; back to a clean state ready for next request
742-
ExecutionContext.Restore(cleanContext);
743751
}
744752
}
745753

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
3636
private readonly HttpConnectionContext _context;
3737
private readonly Http2FrameWriter _frameWriter;
3838
private readonly Pipe _input;
39-
private Task _inputTask;
39+
private readonly Task _inputTask;
4040
private readonly int _minAllocBufferSize;
4141
private readonly HPackDecoder _hpackDecoder;
4242
private readonly InputFlowControl _inputFlowControl;
@@ -85,6 +85,9 @@ public Http2Connection(HttpConnectionContext context)
8585

8686
_context = context;
8787

88+
// Capture the ExecutionContext before dispatching HTTP/2 middleware. Will be restored by streams when processing request
89+
_context.InitialExecutionContext = ExecutionContext.Capture();
90+
8891
_frameWriter = new Http2FrameWriter(
8992
context.Transport.Output,
9093
context.ConnectionContext,
@@ -647,6 +650,7 @@ private Http2StreamContext CreateHttp2StreamContext()
647650
ConnectionInputFlowControl = _inputFlowControl,
648651
ConnectionOutputFlowControl = _outputFlowControl,
649652
TimeoutControl = TimeoutControl,
653+
InitialExecutionContext = _context.InitialExecutionContext,
650654
};
651655
}
652656

src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamOfT.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
54
using Microsoft.AspNetCore.Hosting.Server;
65
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
76
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.IO.Pipelines;
66
using System.Net;
7+
using System.Threading;
78
using Microsoft.AspNetCore.Connections;
89
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -22,5 +23,6 @@ internal class HttpConnectionContext
2223
public IPEndPoint RemoteEndPoint { get; set; }
2324
public ITimeoutControl TimeoutControl { get; set; }
2425
public IDuplexPipe Transport { get; set; }
26+
public ExecutionContext InitialExecutionContext { get; set; }
2527
}
2628
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Connections.Features;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.AspNetCore.Server.Kestrel.Core;
11+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
12+
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
13+
using Microsoft.AspNetCore.Testing;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using Microsoft.Extensions.Logging;
16+
using Xunit;
17+
18+
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2
19+
{
20+
public class Http2EndToEndTests : TestApplicationErrorLoggerLoggedTest
21+
{
22+
[Fact]
23+
public async Task MiddlewareIsRunWithConnectionLoggingScopeForHttp2Requests()
24+
{
25+
var expectedLogMessage = "Log from connection scope!";
26+
string connectionIdFromFeature = null;
27+
28+
var mockScopeLoggerProvider = new MockScopeLoggerProvider(expectedLogMessage);
29+
LoggerFactory.AddProvider(mockScopeLoggerProvider);
30+
31+
await using var server = new TestServer(async context =>
32+
{
33+
connectionIdFromFeature = context.Features.Get<IConnectionIdFeature>().ConnectionId;
34+
35+
var logger = context.RequestServices.GetRequiredService<ILogger<Http2EndToEndTests>>();
36+
logger.LogInformation(expectedLogMessage);
37+
38+
await context.Response.WriteAsync("hello, world");
39+
},
40+
new TestServiceContext(LoggerFactory),
41+
listenOptions =>
42+
{
43+
listenOptions.Protocols = HttpProtocols.Http2;
44+
});
45+
46+
var connectionCount = 0;
47+
using var connection = server.CreateConnection();
48+
49+
using var socketsHandler = new SocketsHttpHandler()
50+
{
51+
ConnectCallback = (_, _) =>
52+
{
53+
if (connectionCount != 0)
54+
{
55+
throw new InvalidOperationException();
56+
}
57+
58+
connectionCount++;
59+
return new ValueTask<Stream>(connection.Stream);
60+
},
61+
};
62+
63+
using var httpClient = new HttpClient(socketsHandler);
64+
65+
using var httpRequsetMessage = new HttpRequestMessage()
66+
{
67+
RequestUri = new Uri("http://localhost/"),
68+
Version = new Version(2, 0),
69+
VersionPolicy = HttpVersionPolicy.RequestVersionExact,
70+
};
71+
72+
using var responseMessage = await httpClient.SendAsync(httpRequsetMessage);
73+
74+
Assert.Equal("hello, world", await responseMessage.Content.ReadAsStringAsync());
75+
76+
Assert.NotNull(connectionIdFromFeature);
77+
Assert.NotNull(mockScopeLoggerProvider.ConnectionLogScope);
78+
Assert.Equal(connectionIdFromFeature, mockScopeLoggerProvider.ConnectionLogScope[0].Value);
79+
}
80+
81+
private class MockScopeLoggerProvider : ILoggerProvider, ISupportExternalScope
82+
{
83+
private readonly string _expectedLogMessage;
84+
private IExternalScopeProvider _scopeProvider;
85+
86+
public MockScopeLoggerProvider(string expectedLogMessage)
87+
{
88+
_expectedLogMessage = expectedLogMessage;
89+
}
90+
91+
public ConnectionLogScope ConnectionLogScope { get; private set; }
92+
93+
public ILogger CreateLogger(string categoryName)
94+
{
95+
return new MockScopeLogger(this);
96+
}
97+
98+
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
99+
{
100+
_scopeProvider = scopeProvider;
101+
}
102+
103+
public void Dispose()
104+
{
105+
}
106+
107+
private class MockScopeLogger : ILogger
108+
{
109+
private readonly MockScopeLoggerProvider _loggerProvider;
110+
111+
public MockScopeLogger(MockScopeLoggerProvider parent)
112+
{
113+
_loggerProvider = parent;
114+
}
115+
116+
public IDisposable BeginScope<TState>(TState state)
117+
{
118+
return _loggerProvider._scopeProvider?.Push(state);
119+
}
120+
121+
public bool IsEnabled(LogLevel logLevel)
122+
{
123+
return true;
124+
}
125+
126+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
127+
{
128+
if (formatter(state, exception) != _loggerProvider._expectedLogMessage)
129+
{
130+
return;
131+
}
132+
133+
_loggerProvider._scopeProvider?.ForEachScope(
134+
(scopeObject, loggerPovider) =>
135+
{
136+
loggerPovider.ConnectionLogScope ??= scopeObject as ConnectionLogScope;
137+
},
138+
_loggerProvider);
139+
}
140+
}
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)