-
Notifications
You must be signed in to change notification settings - Fork 528
Improve implementation of IHttpUpgradeFeature #1706
Conversation
@@ -4,6 +4,7 @@ TestResults/ | |||
.nuget/ | |||
*.sln.ide/ | |||
_ReSharper.*/ | |||
.idea/ |
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.
?
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.
Rider :)
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.
Fix everywhere?
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.
😁
test/shared/TestConnection.cs
Outdated
public Task ReceiveUntil(string line, StringComparison comparer = StringComparison.OrdinalIgnoreCase) | ||
=> ReceiveUntil(next => next.Equals(line, comparer)); | ||
|
||
public async Task ReceiveUntil(Predicate<string> predicate) |
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.
Predicate<string>
, feels like 1999 😄
@@ -256,18 +256,15 @@ public void InitializeStreamsResetsStreams() | |||
|
|||
var originalRequestBody = _frame.RequestBody; | |||
var originalResponseBody = _frame.ResponseBody; | |||
var originalDuplexStream = _frame.DuplexStream; |
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.
Add some unit tests for the Streams
var feature = context.Features.Get<IHttpUpgradeFeature>(); | ||
var stream = await feature.UpgradeAsync(); | ||
|
||
Assert.Throws<InvalidOperationException>(() => context.Response.Body.WriteByte((byte)' ')); |
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.
Does this throw the exception you think it's throwing?
@@ -89,6 +89,9 @@ internal static BadHttpRequestException GetException(RequestRejectionReason reas | |||
case RequestRejectionReason.InvalidHostHeader: | |||
ex = new BadHttpRequestException("Invalid Host header.", StatusCodes.Status400BadRequest); | |||
break; | |||
case RequestRejectionReason.UpgradeRequestCannotHavePayload: | |||
ex = new BadHttpRequestException("Requests with 'Connection: Upgrade' cannot have content in the request body", StatusCodes.Status400BadRequest); |
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: end error message with a period.
"", | ||
""); | ||
await connection.Receive("HTTP/1.1 101 "); | ||
await connection.ReceiveUntil("New protocol data"); |
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.
Assert the exact message you're expecting to receive.
""); | ||
await connection.Receive("HTTP/1.1 101 "); | ||
await connection.ReceiveUntil("New protocol data"); | ||
await upgrade.Task.TimeoutAfter(TimeSpan.FromSeconds(30)); |
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.
Capture the exception and assert that it is exactly what you expect.
await connection.Receive("HTTP/1.1 101 "); | ||
|
||
var sendTask = connection.Send(send + "\r\n"); | ||
var recvTask = connection.ReceiveUntil(recv); |
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.
Assert the exact message you're expecting
} | ||
|
||
[Fact] | ||
public async Task RequestWithContentLengthAndUpgradeThrows() |
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: AreRejected
or Receive400Response
are better than Throws
"", | ||
"1"); | ||
|
||
await connection.Receive("HTTP/1.1 400 "); |
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.
Assert exact message
} | ||
|
||
[Fact] | ||
public async Task RequestWithContentLengthAndUpgradeThrows() |
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.
Add test for Transfer-Encoding: chunked
.
🆙 📅 responded to your feedback and added more testing. |
7b5f1ca
to
d8fc979
Compare
: base(context) | ||
{ | ||
RequestUpgrade = upgrade; | ||
RequestUpgrade = true; | ||
} | ||
|
||
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken) |
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.
Shouldn't this always return new ValueTask<ArraySegment<byte>>();
since the upgrade requests can't have a payload? I think doing otherwise is inviting buffering middleware to interfere.
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.
Currently, this stream is what is eventually used to back the FrameDuplexStream returned by UpgradeAsync()
_state = FrameStreamState.Closed; | ||
if (_state != FrameStreamState.Upgraded) | ||
{ | ||
_state = FrameStreamState.Closed; |
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.
Why not doe this if the _state is Upgraded?
{ | ||
// Can't use dispose (or close) as can be disposed too early by user code | ||
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes | ||
_state = FrameStreamState.Closed; |
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.
ditto.
var upgraded = new FrameRequestStream(this); | ||
if (_state != FrameStreamState.Aborted) | ||
{ | ||
_state = FrameStreamState.Upgraded; |
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.
Why not change state only if Aborted, but not closed? Why still return an upgraded stream?
@@ -209,6 +224,8 @@ private Task<int> ValidateState(CancellationToken cancellationToken) | |||
return _error != null ? | |||
Task.FromException<int>(_error) : | |||
Task.FromCanceled<int>(new CancellationToken(true)); | |||
case FrameStreamState.Upgraded: | |||
return _nullRead; |
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 stream should return nullRead even if it hasn't been upgraded.
@@ -7,6 +7,7 @@ enum FrameStreamState | |||
{ | |||
Open, | |||
Closed, | |||
Aborted | |||
Aborted, | |||
Upgraded |
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 would remove the Upgraded state.
@@ -28,7 +28,7 @@ protected MessageBody(Frame context) | |||
|
|||
public bool RequestUpgrade { get; protected set; } | |||
|
|||
public Task<int> ReadAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken = default(CancellationToken)) | |||
public virtual Task<int> ReadAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken = default(CancellationToken)) |
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.
PeekAsync is already abstract. Use that instead.
|
||
public Stream Upgrade() | ||
{ | ||
_upgradedRequest = _upgradedRequest ?? RequestBody.Upgrade(); |
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 think asking the primary RequestBody stream to return an upgraded version of itself causes some grossness. I don't like either FrameRequestStream or FrameResponseStream having an Upgraded state or an Upgrade method.
I think that an upgradable request should have completely fake RequestBody and ResponseBodys that no-op. The upgraded stream should be built from the real streams that aren't accessible via the HttpContext directly.
@@ -0,0 +1,123 @@ | |||
<?xml version="1.0" encoding="utf-8"?> |
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.
Why?
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.
So we can assert the exception being thrown has the message we expect. Per this comment: #1706 (comment)
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.
Now that we have this, I think we should move all our user-facing strings to this file (not in this PR of course). Can you file a task for that?
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.
🆙 📅 rewritten per @halter73's feedback |
return _upgradeStream; | ||
} | ||
|
||
public (Stream request, Stream response) Start(MessageBody body) |
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.
+1 Pranav Points
|
||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure | ||
{ | ||
internal class WrappedStream : Stream |
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: maybe call this WrappingStream
256ca59
to
df5984e
Compare
Plaintext benchmark: lots of noise on dev, but appears average perf is stable. dev
namc/upgrade
|
using System; | ||
using System.IO; | ||
#if NET46 | ||
using System.Runtime.Remoting; |
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 this?
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.
System.Runtime.Remoting.ObjRef
} | ||
|
||
[Fact] | ||
public async Task RejectsRequestWithChunckedEncodingAndUpgrade() |
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.
typo: Chuncked
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.
fxied
df5984e
to
4382a2e
Compare
{ | ||
using (var connection = server.CreateConnection()) | ||
{ | ||
await connection.Send("POST / HTTP/1.1", |
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 case should be in a separate test.
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.
done
After upgrade has been accepted by the server: - Reads to HttpRequest.Body always return 0 - Writes to HttpResponse.Body always throw - The only valid way to communicate is to use the stream returned by IHttpUpgradeFeature.UpgradeAsync() Also, Kestrel returns HTTP 400 if requests attempt to send a request body along with Connection: Upgrade
4382a2e
to
ee9feed
Compare
After upgrade has been accepted by the server:
Also, Kestrel returns HTTP 400 if requests attempt to send a request body along with Connection: Upgrade. Resolves #1570