POST+Upgrade, or unexpected limitation of RFC8441

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 10:28:41 UTC