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

Improve implementation of IHttpUpgradeFeature #1706

Merged
merged 1 commit into from
Apr 20, 2017
Merged

Conversation

natemcmaster
Copy link
Contributor

@natemcmaster natemcmaster commented Apr 18, 2017

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. Resolves #1570

@@ -4,6 +4,7 @@ TestResults/
.nuget/
*.sln.ide/
_ReSharper.*/
.idea/
Copy link
Member

Choose a reason for hiding this comment

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

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rider :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Fix everywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😁

public Task ReceiveUntil(string line, StringComparison comparer = StringComparison.OrdinalIgnoreCase)
=> ReceiveUntil(next => next.Equals(line, comparer));

public async Task ReceiveUntil(Predicate<string> predicate)
Copy link
Member

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

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

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

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

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

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

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

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

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

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.

@natemcmaster
Copy link
Contributor Author

🆙 📅 responded to your feedback and added more testing.

: base(context)
{
RequestUpgrade = upgrade;
RequestUpgrade = true;
}

protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)
Copy link
Member

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.

Copy link
Contributor Author

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

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

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

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

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

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

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

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

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor Author

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)

Copy link
Contributor

@cesarblum cesarblum Apr 19, 2017

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@natemcmaster
Copy link
Contributor Author

🆙 📅 rewritten per @halter73's feedback

return _upgradeStream;
}

public (Stream request, Stream response) Start(MessageBody body)
Copy link
Member

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

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

@natemcmaster natemcmaster force-pushed the namc/upgrade branch 2 times, most recently from 256ca59 to df5984e Compare April 20, 2017 16:07
@natemcmaster
Copy link
Contributor Author

Plaintext benchmark: lots of noise on dev, but appears average perf is stable.

dev

RPS: 1317002.91
RPS: 1334182.6
RPS: 1265391.26
RPS: 1283916.25

namc/upgrade

RPS: 1283744.76
RPS: 1292864.19
RPS: 1274556.47
RPS: 1290861.72

using System;
using System.IO;
#if NET46
using System.Runtime.Remoting;
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

typo: Chuncked

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fxied

{
using (var connection = server.CreateConnection())
{
await connection.Send("POST / HTTP/1.1",
Copy link
Contributor

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.

Copy link
Contributor Author

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
@natemcmaster natemcmaster merged commit ee9feed into dev Apr 20, 2017
@natemcmaster natemcmaster deleted the namc/upgrade branch April 20, 2017 22:42
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