Skip to content

HTTP2: Support bidirectional content #1511

@Tratcher

Description

@Tratcher

Update & re-scoping of the issue

#1511 (comment)
Remaining work: We added an internal AllowDuplex property on HttpContent. We should consider making this public.

Old Update

As part of 3.0 we essentially disabled bidirectional HttpContent, implementing only what was needed for gRPC support. We now need to, as part of 5.0/Future, consider how to enable general-purpose bidirectional support.

Original issue

dotnet/corefx#36884 (HTTP/2 100 Continue) also snuck in bidirectional support (https://github.com/dotnet/corefx/issues/36855) for HTTP/2. This works pretty well but does introduce an ambiguous scenario when the server completes the response before the client completes the request.

Server code: (Kestrel 3.0.0-preview7)

                var response = "hello, world";
                context.Response.ContentLength = response.Length;
                await context.Response.WriteAsync(response);

                await context.Features.Get<IHttpResponseCompletionFeature>().CompleteAsync();
                await Task.Delay(TimeSpan.FromSeconds(5));
                // Drain the request body
                await context.Request.Body.CopyToAsync(Stream.Null);

Yes this server code is a bit unusual, it was written this way to make the error visible from CopyToAsync. You'll get similar behavior if you just write Hello World and let the request unwind.

Client code:

            var client = new HttpClient();
            client.DefaultRequestVersion = new Version(2, 0);
            using var file = File.OpenRead(@"A200MbFile.txt");
            var content = new StreamContent(file);
            var response = await client.PostAsync("https://localhost:5001", content);
  • In HTTP/1.x this works fine, PostAsync won't complete until the request stream has been completely uploaded.
  • In HTTP/2 it's ambiguous because when PostAsync completes you have a fully buffered response body but you have no idea what the state of the request is nor a way to be notified of its completion. Trying to perform any operations on that file can cause errors in the client code because it's still in use. If you close the file now the request will fail, but the error will only be visible on the server.
  • HttpCompletionOptions do not have any affect on the request, they only affect the response.
  • None of the built in HttpContent implementations provide any visibility on their progress. PushStreamContent from WebApi is the only HttpContent example I've seen that would surface this state to users.

Mitigations:

  • Servers usually wait for the request body to end before ending the response body. However the client can't always rely on the server behaving a specific way, ending the response early is within spec.
  • For >= 300 responses the HTTP/2 stream will be reset and the request stream abandon. These are the most common scenarios where the server would end the response without draining the request. This doesn't surface any errors directly to the caller unless they have a custom HttpContent, they have to infer the request body is done/abandon from the status code.

These mitigations make the issue occur infrequently enough that it will cause some very non-obvious issues when it it does happen (e.g. locked files, InvalidOperationExceptions, multi-threaded stream re-use, etc.).

Proposals:
A) At a minimum there needs to be guidance given that this might happen and how the user might handle it. I don't know what guidance could be given beyond implementing a custom HttpContent.
B) Add something to HttpContent to make progress more visible. E.g. something they could await or DisposeAsync that could wait for things to finish first. This wouldn't be very discoverable and would need to be accompanied by guidance.
C) Only enable bidirectional support when using HttpCompletionOptions.ResponseHeadersRead (or a new flag?). Most people don't use this flag so they wouldn't be impacted by the behavior change (e.g. PostAsync doesn't support that mode). I'm not sure how you implement this without B.

@wfurt @davidsh

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions