-
Notifications
You must be signed in to change notification settings - Fork 447
Copy HttpContext properties for long polling transport #1684
Conversation
- The long polling transport simulates a persistent connection over multiple http requests. In order to expose common http request properties, we need to copy them to a fake http context on the first poll and set that as the HttpContext exposed via the IHttpContextFeature.
- Set the current TraceIdentifier on the fake HttpContext. The trace identifier will reflect accurately in the logs. - Unskip tests
features.Set<IHttpConnectionFeature>(connectionFeature); | ||
|
||
// REVIEW: We could strategically look at adding other features but it might be better | ||
// if the expose a callback that would allow the user to preserve HttpContext properties. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we expose a callback...
newHttpContext.TraceIdentifier = context.TraceIdentifier; | ||
newHttpContext.User = context.User; | ||
|
||
// Making request services function property could be tricky and expensive as it would require |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Not sure I understand.
Is this something that should be investigated further later on?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yah, I can remove the comment but I wanted to make it visible in the review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment is fine. Maybe log an issue and link to it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do DI scopes work with signalr? Why would it be tricky/expensive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SignalR makes a scope per hub invocation which works great. The RequestServices on the other hand would exist for the lifetime of the connection. We'd then need to dispose the scope when the connection is disposed, that in itself isn't too bad. Where it gets wonky is when somebody tries to get the same instance of a service in middleware and then in a Hub. It'll be different (which is by design and most likely fine). The strange thing about it is that RequestServices would have been replaced.
@@ -540,7 +540,17 @@ public class HubEndpointTests | |||
client.Dispose(); | |||
|
|||
// Ensure the client channel is empty | |||
Assert.Null(client.TryRead()); | |||
var message = client.TryRead(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to "fix" this test since it kept failing with the close message thing.
/cc @JamesNK
newHttpContext.RequestServices = EmptyServiceProvider.Instance; | ||
|
||
// REVIEW: This extends the lifetime of anything that got put into HttpContext.Items | ||
newHttpContext.Items = context.Items.ToDictionary(p => p.Key, p => p.Value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. To be honest I'm not 100% sure how HttpContext
works in websockets land. Is there middleware that would place values in Items
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, would this work to copy the dictionary:
newHttpContext.Items = new Dictionary<object, object>(context.Items);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. To be honest I'm not 100% sure how HttpContext works in websockets land. Is there middleware that would place values in Items?
The HttpContext is fine in server sent events and websockets, it's in tact because the request doesn't complete until the connection is dead.
Also, would this work to copy the dictionary:
I'll do that not sure why I LINQified this thing 😄
Assert.Equal(4563, connectionHttpContext.Connection.LocalPort); | ||
Assert.Equal(IPAddress.IPv6Any, connectionHttpContext.Connection.RemoteIpAddress); | ||
Assert.Equal(43456, connectionHttpContext.Connection.RemotePort); | ||
Assert.NotNull(connectionHttpContext.RequestServices); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also check that doing stuff with the response has no effect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll assert something.
{ | ||
case CloseMessage close: | ||
break; | ||
case null: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just remove this case since it would be handled by the default case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. Will do.
@@ -336,6 +338,90 @@ public HttpConnectionDispatcherTests(ITestOutputHelper output) : base(output) | |||
} | |||
} | |||
|
|||
[Fact] | |||
public async Task HttpContextFeatureForLongpollingWorksBetweenPolls() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure @moozzyk would love how long this test is 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:D
🆙 📅 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more thing. I don't know all of gotchas of long polling so wait for more approves before merge
private class EmptyServiceProvider : IServiceProvider | ||
{ | ||
public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider(); | ||
public object GetService(Type serviceType) => null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this should throw an error explaining why RequestServices
doesn't work as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will at least give a proper exception message when somebody is asking for a required service. That's why I didn't leave it as null. Push come to shove, we can implement something where we make a scoped service provider. I just wanted to see what people think of this first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the error message? I was thinking that calling GetService would throw an error explaining why it isn't supported rather than return null and lead to a hard to debug NRE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usage pattern is that the caller does GetService in order to get a null and we have a GetRequiredService that throws if GetService returns null. If anyone is calling GetService today and not expecting null then they already have an NRE
np |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still a little unsure about this. I don't really like it, but I'm not sure the other hacks are any better...
else | ||
{ | ||
// Set the request trace identifier to the current http request handling the poll | ||
existing.TraceIdentifier = context.TraceIdentifier; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Any other properties we want to update? Connection info (IP/Port/etc; In case a client changes networks, etc.)?
Of course, this is run in parallel with existing invocations so the data could "shear" (but as long as each set is atomic, it won't break anything).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extremely unlikely this would matter in real life. The reason I changed the trace identifier is because we log it to tell the user what the previous connection id is.
newHttpContext.RequestServices = EmptyServiceProvider.Instance; | ||
|
||
// REVIEW: This extends the lifetime of anything that got put into HttpContext.Items | ||
newHttpContext.Items = new Dictionary<object, object>(context.Items); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This worries me a bit... people use this to cache per-request stuff and we don't want to make those items live longer than the actual request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This worries me a bit... people use this to cache per-request stuff and we don't want to make those items live longer than the actual request.
Yea, thats why I commented.
connection.User = context.User; | ||
connection.SetHttpContext(context); | ||
// For long polling, the requests come and go but the connection is still alive. | ||
// to make the IHttpContextFeature work well, we make a copy of the relevant properties |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: To
else | ||
{ | ||
// Set the request trace identifier to the current http request handling the poll | ||
existing.TraceIdentifier = context.TraceIdentifier; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we similarly set the existing.User
value? Although we use connection.User
for the Hub method auth
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
over multiple http requests. In order to expose common http request
properties, we need to copy them to a fake http context on the first poll
and set that as the HttpContext exposed via the IHttpContextFeature.
Fixes #1644