Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Copy HttpContext properties for long polling transport #1684

Merged
merged 8 commits into from
Mar 22, 2018

Conversation

davidfowl
Copy link
Member

  • 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.

Fixes #1644

- 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.
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

@davidfowl davidfowl Mar 22, 2018

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.

Copy link
Contributor

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

Copy link
Member

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?

Copy link
Member Author

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();
Copy link
Member Author

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);
Copy link
Member

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?

Copy link
Member

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);

Copy link
Member Author

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);
Copy link
Member

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?

Copy link
Member Author

@davidfowl davidfowl Mar 22, 2018

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:
Copy link
Contributor

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

Copy link
Member Author

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()
Copy link
Contributor

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 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:D

@davidfowl
Copy link
Member Author

🆙 📅

Copy link
Member

@JamesNK JamesNK left a 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;
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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

@davidfowl
Copy link
Member Author

One more thing. I don't know all of gotchas of long polling so wait for more approves before merge

np

Copy link
Contributor

@analogrelay analogrelay left a 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;
Copy link
Contributor

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).

Copy link
Member Author

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);
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Member

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;
Copy link
Member

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@davidfowl davidfowl merged commit f1a3775 into release/2.1 Mar 22, 2018
@davidfowl davidfowl deleted the davidfowl/http-context-longpolling branch March 28, 2018 06:04
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants