Re: POST+Upgrade, or unexpected limitation of RFC8441

I also found it funny to observe in the docker documentation the section
onn interactive sessions [1], with our old fried h2c upgrade

> This endpoint hijacks the HTTP connection to HTTP2 transport that allows
the client to expose gPRC services on that connection.

RFC 7540 has the following to say about h2c upgrade [2]:

> Requests that contain a payload body MUST be sent in their entirety
before the client can send HTTP/2 frames. This means that a large request
can block the use of the connection until it is completely sent.

Anyone got the history about why that text was added?

Cheers
Lucas

[1] https://docs.docker.com/reference/api/engine/version/v1.43/

[2] https://datatracker.ietf.org/doc/html/rfc7540#section-3.2

On Wed, 18 Dec 2024, 10:50 Lucas Pardue, <lucaspardue.24.7@gmail.com> wrote:

> Hi Willy,
>
> Just a brief reply that this sounds like its also related to
> draft-ietf-httpbis-optimistic-upgrade [1].
>
> Cheers
> Lucas
>
> [1]
> https://httpwg.org/http-extensions/draft-ietf-httpbis-optimistic-upgrade.html
>
>
>
> On Wed, 18 Dec 2024, 10:33 Willy Tarreau, <w@1wt.eu> wrote:
>
>> Hi!
>>
>> RFC8441 ("Bootstrapping WebSockets with HTTP/2") indicates how
>> to deal with an Upgrade over HTTP/2. To make it short, HTTP/1's
>> Connection+Upgrade->101 becomes a CONNECT+:protocol->200 in H2.
>> The RFC specifies a :protocol pseudo-header that allows to use
>> the mechanism beyond Websocket.
>>
>> A while ago, we've implemented the mechanism in haproxy and we
>> support the upgrade from H1+Upgrade to H2+CONNECT. So far it
>> works fine for our users.
>>
>> A few months ago we got a report of breakage[1] that unveiled
>> something unexpected: given that CONNECT doesn't support a
>> request body, we had simplified our support for Upgrade and
>> completely rejected any request body at the H1 layer (so that
>> we could fail early when not knowing if the output would be in
>> H1 or H2).  But doing so we accidentally broke support for the
>> "Docker Engine API" protocol over H1. That's how we discovered
>> that some protocols were relying on POST + Upgrade! (maybe it's
>> the only one, I don't know, it was the first report in a few
>> years of existence).
>>
>> Thus, for now we've worked around the problem so that our code
>> now rejects the upgrade only on the outgoing H2 side when facing
>> a body, but the problem remains that the mechanism proposed by
>> RFC8441 doesn't offer provisions for completely transporting H1
>> over H2, nor replacing H1 with H2. Indeed, users of this docker
>> engine protocol will need to limit themselves to H1 when talking
>> to servers or other proxies. It's a bit sad, and I liked the idea
>> of using the semantically-close CONNECT for this.
>>
>> To me, it looks like the limitations are shared among multiple
>> causes:
>>   - the docker engine API relies on upgrades after a POST, which are
>>     not much common, and even never mentioned in any of the upgrade
>>     specs (2817, 7230, 9110), though common sense dictates that it's
>>     expected to work fine since the client cannot know whether the
>>     server will accept the upgrade or not. I.e. it's natural to
>>     think that the server is expected to consume the whole request
>>     body before upgrading, or at least drain it after rejecting the
>>     upgrade.
>>
>>   - RFC8441 that proposes to use the CONNECT method for the tunnel,
>>     while CONNECT itself is defined as "not having content" (9110)
>>     or "not having semantics" (7231), which looked fine for websocket
>>     but not necessarily for others (that were not identified by then).
>>
>>   - the use of RFC8441 for non-websocket protocols (but if not usable,
>>     what could be the point of ":protocol" ?)
>>
>> I really don't know what could reasonably be done at this point to
>> address that incompatibility. Here are a few ideas:
>>   - we could decide that in the context of RFC8441, CONNECT works a bit
>>     like GET in that by default the request has no message body unless
>>     it is explicitly advertised, and in this case the request cannot
>>     succeed until it's entirely consumed. That sounds quite reasonable
>>     since this would remove exceptions, and not change anything for
>>     existing implementations that don't look for that message body.
>>     That does not solve the situation for request bodies sent using
>>     chunked encoding however.
>>
>>   - we could imagine a new method that does like CONNECT but also
>>     covers the message body. Same as above, there are still no
>>     provisions in H2+ to transmit a chunked body and delimit the
>>     part that belongs to the request message and the part that
>>     is to be tunnelled. And I'm personally not convinced about the
>>     improved interoperability of a new method.
>>
>>   - we could explicitly state that POST+Upgrade is strictly forbidden,
>>     but we now know it's already in use and working fine for at least
>>     one implementation.
>>
>>   - or we could do nothing and consider that some parts of HTTP/1 will
>>     remain HTTP/1 forever and will not be transportable over newer
>>     versions. I'm not fundamentally against it but it would warrant
>>     some extra documentation (especially in the H1 related specs) to
>>     discourage such use so that we make sure no new protocol adopts
>>     them and stay stuck in a corner.
>>
>> I'd say that the first approach (tolerating content-length with
>> CONNECT when :protocol is used) has my preference. It will not
>> permit chunked requests but should cover the vast majority of use
>> cases where the server expects a body before deciding to upgrade
>> (after all not all HTTP/1 servers support chunked requests either).
>>
>> But I'm interesting in ideas and opinions others might have.
>>
>> Thanks!
>> Willy
>>
>> [1] https://github.com/haproxy/haproxy/issues/2684
>>
>>

Received on Wednesday, 18 December 2024 11:10:49 UTC