aspnetcore: HttpContextAccessor.HttpContext is null when using TestServer and .net 2.2.2
Describe the bug
When using .net core 2.2.2 along with the TestServer(https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/TestHost/src/TestServer.cs), HttpContextAccessor.HttpContext will return null in certain situations. We’re experiencing this is the repro: https://github.com/Microsoft/fhir-server/
If you have 2.2.1 installed, this behavior is not exhibited. It appears related to the use of authentication and calls that are made on the JWT backchannel handler.
To Reproduce
Steps to reproduce the behavior:
- Clone my repro repository here: https://github.com/brandonpollett/TestServerRepro
- Execute the unit tests with .net 2.2.2 and you will see a test failure
- Remove .net 2.2.2 and run with 2.2.1 and the tests will succeed
- Note: in this sample repo the null reference occurs at line 237 of https://github.com/IdentityServer/IdentityServer4/blob/master/src/Services/Default/DefaultUserSession.cs, but in the actual repro, the null reference is on line 62 of https://github.com/Microsoft/fhir-server/blob/master/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs
Expected behavior
The tests should pass while using 2.2.2 and 2.2.1
Additional context
When the unit tests pass the output of dotnet --info is:
C:\projects\TestServerRepro [master ≡]> dotnet --info
.NET Core SDK (reflecting any global.json):
Version: 2.2.103
Commit: 8edbc2570a
Runtime Environment:
OS Name: Windows
OS Version: 10.0.17763
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\2.2.103\
Host (useful for support):
Version: 2.2.1
Commit: 878dd11e62
.NET Core SDKs installed:
1.0.4 [C:\Program Files\dotnet\sdk]
1.1.0 [C:\Program Files\dotnet\sdk]
2.0.0 [C:\Program Files\dotnet\sdk]
2.0.2 [C:\Program Files\dotnet\sdk]
2.1.2 [C:\Program Files\dotnet\sdk]
2.1.4 [C:\Program Files\dotnet\sdk]
2.1.100-preview-007354 [C:\Program Files\dotnet\sdk]
2.1.100-preview-007363 [C:\Program Files\dotnet\sdk]
2.1.100 [C:\Program Files\dotnet\sdk]
2.1.103 [C:\Program Files\dotnet\sdk]
2.1.104 [C:\Program Files\dotnet\sdk]
2.1.200 [C:\Program Files\dotnet\sdk]
2.1.201 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.402 [C:\Program Files\dotnet\sdk]
2.1.403 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.2.103 [C:\Program Files\dotnet\sdk]
.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 1.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 1.1.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
When failing the output of dotnet--info is:
C:\projects\TestServerRepro [master ≡]> dotnet --info
.NET Core SDK (reflecting any global.json):
Version: 2.2.104
Commit: 73f036d4ac
Runtime Environment:
OS Name: Windows
OS Version: 10.0.17763
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\2.2.104\
Host (useful for support):
Version: 2.2.2
Commit: a4fd7b2c84
.NET Core SDKs installed:
1.0.4 [C:\Program Files\dotnet\sdk]
1.1.0 [C:\Program Files\dotnet\sdk]
2.0.0 [C:\Program Files\dotnet\sdk]
2.0.2 [C:\Program Files\dotnet\sdk]
2.1.2 [C:\Program Files\dotnet\sdk]
2.1.4 [C:\Program Files\dotnet\sdk]
2.1.100-preview-007354 [C:\Program Files\dotnet\sdk]
2.1.100-preview-007363 [C:\Program Files\dotnet\sdk]
2.1.100 [C:\Program Files\dotnet\sdk]
2.1.103 [C:\Program Files\dotnet\sdk]
2.1.104 [C:\Program Files\dotnet\sdk]
2.1.200 [C:\Program Files\dotnet\sdk]
2.1.201 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.402 [C:\Program Files\dotnet\sdk]
2.1.403 [C:\Program Files\dotnet\sdk]
2.1.503 [C:\Program Files\dotnet\sdk]
2.2.104 [C:\Program Files\dotnet\sdk]
.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 1.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 1.1.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 6
- Comments: 29 (19 by maintainers)
Commits related to this issue
- suppress ExecutionContext by default in TestServer fixes #7975 There is a 'PreserveExecutionContext' property to turn the old behavior back on. Also I had to modify where IHttpApplication.CreateCont... — committed to dotnet/aspnetcore by analogrelay 5 years ago
- suppress ExecutionContext by default in TestServer fixes #7975 There is a 'PreserveExecutionContext' property to turn the old behavior back on. Also I had to modify where IHttpApplication.CreateCont... — committed to dotnet/aspnetcore by analogrelay 5 years ago
- suppress ExecutionContext by default in TestServer (#10094) fixes #7975 There is a 'PreserveExecutionContext' property to turn the old behavior back on. Also I had to modify where IHttpApplication... — committed to dotnet/aspnetcore by analogrelay 5 years ago
- fix https://github.com/ShokoAnime/ShokoServer/issues/783; Added 1.0 header for /Stream/ endpoint; Bypassed the ASPNetCore ? error ?; Endpoint working correct; — committed to ShokoAnime/ShokoServer by bigretromike 5 years ago
This workaround unblocked us. Our scenario is this: We have multiple microservices. We use JWT Bearer auth and Open ID Connect using our own IdentityServer4 IdP. In order for our tests to work we need to set up two TestServers: the microservice we are testing and the IdP. Then we delegate the JWT verification from our microservice to our IdP.
This regression is unfortunate, but glad we’re unblocked now. I’m not fully understanding the reasoning why this is an edge case. It seems like a common case for anyone testing with authentication. Thanks for the workaround @anurse .
Thanks @parekhkb, that’s a more concrete example and it’s not even recursive, it’s multi-tier. It makes sense we’d need to prevent cross contamination between the server instances.
I can do one better and get a full-on workaround by adding an HttpMessageHandler that abandons the ExecutionContext for you. Then you just have to wrap the ClientHandler up in that and it seems to work:
My main concern is the unintended consequences of abandoning the EC here. Perhaps this would live best as a known-issue workaround rather than a fix.
Looks like there is indeed a regression here. The problem lies in the nested HTTP calls. When you make a call back to the application using the HttpClient produced by
TestServer.CreateClient()(or handlers produced byTestServer.CreateHandler()) the HttpContextAccessor gets cleared out after the nested call is made and isn’t restored to it’s previous content.This behavior does not appear to occur in 2.2.0. After a brief binary search it appears to have been introduces in 2.2.2. I do see a change to HttpContextAccessor in 2.2.2 (https://github.com/aspnet/AspNetCore/pull/6036 by @JunTaoLuo), which definitely seems suspiciously relevant 😃. I think something in that PR regressed this TestServer scenario where there are nested HTTP calls.
Repro details below. cc @Tratcher @davidfowl @JunTaoLuo
The following program repros the issue
If I build this and then run it against 2.2.0, I get:
If I run against 2.2.2, I get:
I’ll look at making this change and adding a setting to turn it back off. Technically a breaking change which will need some announcement.
The workaround provided by @anurse is here https://github.com/aspnet/AspNetCore/issues/7975#issuecomment-481536061
The thing we lose is fairly simple, it’s the flow of ExecutionContext between the “client” and the “server”. That means
AsyncLocaland things like current culture. Since TestServer is designed to emulate a real client/server interaction, and a real client/server interaction has a machine boundary, this seems reasonable. The fact that we flow the EC at all is entirely misleading, and unlike suppressing 500s I think it reduces the value and usefulness of TestServer rather than improving it. I agree that where it adds value, we can compromise the “emulation” but this does not add value, it reduces it.Of course there is. We literally give you the ability to create
HttpMessageHandlers that can “invoke” the server. Regardless of if it was intentional, we definitely gave users the ability to make recursive calls.A user trying to flow
AsyncLocals between the client and server in this scenario seems like the edge case and is not the intended design. I don’t think we should optimize for the edge case (that doesn’t work in a real server) here.To me, disabling EC flow by default seems like the best option here and in 3.0 it can be relatively easily justified as a potential breaking change. We can annoucement it, do it in an early preview and provide an opt-out switch.
💯 agree. Just want to make sure we have an escape hatch because it will break some code (arguably wrong code 😉). An option to “not abandon the EC” seems reasonable to me.
Returning a value from
UnsafeQueueUserWorkItemseems to introduce more complexity than just suppressing the flow fromTask.Run, since you have to dance with a TCS, but whatever. As long as you abandon the EC somehow 😃Ok, I’ll throw this in to preview 6 as low pri. We’ll see what we can pull out here. This seems like another area I might be able to prove I can still write code ;P.
@brandonpollett if you want a workaround for now, see my comment above.