Skip to content

Commit d05c9f4

Browse files
authored
HeaderPropagation: reset AsyncLocal per request (#18300)
As Kestrel can bleed the AsyncLocal across requests, see #13991.
1 parent 7fa6d19 commit d05c9f4

File tree

2 files changed

+66
-19
lines changed

2 files changed

+66
-19
lines changed

src/Middleware/HeaderPropagation/src/HeaderPropagationMiddleware.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public HeaderPropagationMiddleware(RequestDelegate next, IOptions<HeaderPropagat
3333
_values = values ?? throw new ArgumentNullException(nameof(values));
3434
}
3535

36-
public Task Invoke(HttpContext context)
36+
// This needs to be async as otherwise the AsyncLocal could bleed across requests, see https://github.com/aspnet/AspNetCore/issues/13991.
37+
public async Task Invoke(HttpContext context)
3738
{
3839
// We need to intialize the headers because the message handler will use this to detect misconfiguration.
3940
var headers = _values.Headers ??= new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
@@ -56,7 +57,7 @@ public Task Invoke(HttpContext context)
5657
}
5758
}
5859

59-
return _next.Invoke(context);
60+
await _next.Invoke(context);
6061
}
6162

6263
private static StringValues GetValue(HttpContext context, HeaderPropagationEntry entry)

src/Middleware/HeaderPropagation/test/HeaderPropagationMiddlewareTest.cs

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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+
using System;
5+
using System.Collections.Generic;
46
using System.Threading.Tasks;
57
using Microsoft.AspNetCore.Http;
68
using Microsoft.Extensions.Options;
@@ -14,7 +16,11 @@ public class HeaderPropagationMiddlewareTest
1416
public HeaderPropagationMiddlewareTest()
1517
{
1618
Context = new DefaultHttpContext();
17-
Next = ctx => Task.CompletedTask;
19+
Next = ctx =>
20+
{
21+
CapturedHeaders = State.Headers;
22+
return Task.CompletedTask;
23+
};
1824
Configuration = new HeaderPropagationOptions();
1925
State = new HeaderPropagationValues();
2026
Middleware = new HeaderPropagationMiddleware(Next,
@@ -24,8 +30,10 @@ public HeaderPropagationMiddlewareTest()
2430

2531
public DefaultHttpContext Context { get; set; }
2632
public RequestDelegate Next { get; set; }
33+
public Action Assertion { get; set; }
2734
public HeaderPropagationOptions Configuration { get; set; }
2835
public HeaderPropagationValues State { get; set; }
36+
public IDictionary<string, StringValues> CapturedHeaders { get; set; }
2937
public HeaderPropagationMiddleware Middleware { get; set; }
3038

3139
[Fact]
@@ -39,8 +47,8 @@ public async Task HeaderInRequest_AddCorrectValue()
3947
await Middleware.Invoke(Context);
4048

4149
// Assert
42-
Assert.Contains("in", State.Headers.Keys);
43-
Assert.Equal(new[] { "test" }, State.Headers["in"]);
50+
Assert.Contains("in", CapturedHeaders.Keys);
51+
Assert.Equal(new[] { "test" }, CapturedHeaders["in"]);
4452
}
4553

4654
[Fact]
@@ -53,7 +61,7 @@ public async Task NoHeaderInRequest_DoesNotAddIt()
5361
await Middleware.Invoke(Context);
5462

5563
// Assert
56-
Assert.Empty(State.Headers);
64+
Assert.Empty(CapturedHeaders);
5765
}
5866

5967
[Fact]
@@ -66,7 +74,7 @@ public async Task HeaderInRequest_NotInOptions_DoesNotAddIt()
6674
await Middleware.Invoke(Context);
6775

6876
// Assert
69-
Assert.Empty(State.Headers);
77+
Assert.Empty(CapturedHeaders);
7078
}
7179

7280
[Fact]
@@ -82,10 +90,10 @@ public async Task MultipleHeadersInRequest_AddAllHeaders()
8290
await Middleware.Invoke(Context);
8391

8492
// Assert
85-
Assert.Contains("in", State.Headers.Keys);
86-
Assert.Equal(new[] { "test" }, State.Headers["in"]);
87-
Assert.Contains("another", State.Headers.Keys);
88-
Assert.Equal(new[] { "test2" }, State.Headers["another"]);
93+
Assert.Contains("in", CapturedHeaders.Keys);
94+
Assert.Equal(new[] { "test" }, CapturedHeaders["in"]);
95+
Assert.Contains("another", CapturedHeaders.Keys);
96+
Assert.Equal(new[] { "test2" }, CapturedHeaders["another"]);
8997
}
9098

9199
[Theory]
@@ -101,7 +109,7 @@ public async Task HeaderEmptyInRequest_DoesNotAddIt(string headerValue)
101109
await Middleware.Invoke(Context);
102110

103111
// Assert
104-
Assert.DoesNotContain("in", State.Headers.Keys);
112+
Assert.DoesNotContain("in", CapturedHeaders.Keys);
105113
}
106114

107115
[Theory]
@@ -127,8 +135,8 @@ public async Task UsesValueFilter(string[] filterValues, string[] expectedValues
127135
await Middleware.Invoke(Context);
128136

129137
// Assert
130-
Assert.Contains("in", State.Headers.Keys);
131-
Assert.Equal(expectedValues, State.Headers["in"]);
138+
Assert.Contains("in", CapturedHeaders.Keys);
139+
Assert.Equal(expectedValues, CapturedHeaders["in"]);
132140
Assert.Equal("in", receivedName);
133141
Assert.Equal(new StringValues("value"), receivedValue);
134142
Assert.Same(Context, receivedContext);
@@ -145,8 +153,8 @@ public async Task PreferValueFilter_OverRequestHeader()
145153
await Middleware.Invoke(Context);
146154

147155
// Assert
148-
Assert.Contains("in", State.Headers.Keys);
149-
Assert.Equal("test", State.Headers["in"]);
156+
Assert.Contains("in", CapturedHeaders.Keys);
157+
Assert.Equal("test", CapturedHeaders["in"]);
150158
}
151159

152160
[Fact]
@@ -159,7 +167,7 @@ public async Task EmptyValuesFromValueFilter_DoesNotAddIt()
159167
await Middleware.Invoke(Context);
160168

161169
// Assert
162-
Assert.DoesNotContain("in", State.Headers.Keys);
170+
Assert.DoesNotContain("in", CapturedHeaders.Keys);
163171
}
164172

165173
[Fact]
@@ -174,8 +182,46 @@ public async Task MultipleEntries_AddsFirstToProduceValue()
174182
await Middleware.Invoke(Context);
175183

176184
// Assert
177-
Assert.Contains("in", State.Headers.Keys);
178-
Assert.Equal("Test", State.Headers["in"]);
185+
Assert.Contains("in", CapturedHeaders.Keys);
186+
Assert.Equal("Test", CapturedHeaders["in"]);
187+
}
188+
189+
[Fact]
190+
public async Task HeaderInRequest_WithBleedAsyncLocal_HasCorrectValue()
191+
{
192+
// Arrange
193+
Configuration.Headers.Add("in");
194+
195+
// Process first request
196+
Context.Request.Headers.Add("in", "dirty");
197+
await Middleware.Invoke(Context);
198+
199+
// Process second request
200+
Context = new DefaultHttpContext();
201+
Context.Request.Headers.Add("in", "test");
202+
await Middleware.Invoke(Context);
203+
204+
// Assert
205+
Assert.Contains("in", CapturedHeaders.Keys);
206+
Assert.Equal(new[] { "test" }, CapturedHeaders["in"]);
207+
}
208+
209+
[Fact]
210+
public async Task NoHeaderInRequest_WithBleedAsyncLocal_DoesNotHaveIt()
211+
{
212+
// Arrange
213+
Configuration.Headers.Add("in");
214+
215+
// Process first request
216+
Context.Request.Headers.Add("in", "dirty");
217+
await Middleware.Invoke(Context);
218+
219+
// Process second request
220+
Context = new DefaultHttpContext();
221+
await Middleware.Invoke(Context);
222+
223+
// Assert
224+
Assert.Empty(CapturedHeaders);
179225
}
180226
}
181227
}

0 commit comments

Comments
 (0)