Open mikeal opened 4 years ago
This might be a duplicate of https://github.com/nodejs/node/issues/16256 but it’s unclear if that was actually resolved, it certainly doesn’t seem to be documented yet.
It's definitely possible but the flow is a bit difficult. Essentially there are three possible approaches:
Use an initial http1 request with an upgrade header. This requires that the server side be capable of upgrading to http2 from http1 on request. The assumption here is that the server will upgrade to http2 and respond to the original http1 request. This obviously is tricky when it comes to anything other than GET. On the client side, you need a client that can send an http1 request and upgrade to http2 if, and only if, the server upgrades the connection. Given the completely different client code paths, that's not easy.
Use tls alpn to negotiate the http version. This requires that your tls Terminator has the ability to select the version, and that you have a tls client that can use one or the other. This is simpler but means you have to manage the tls socket on the client side separately from the http connection, so using the existing https client and http2 client is more difficult.
Use altsvc to advertise support for http2. This requires preflighting with an options request to solicit an altsvc response, then going on from there. There's currently no support for this in core
Also consider that this is going to be even more difficult with the addition of quic/http3. There's no way to upgrade or failover since it's udp instead of tcp.
The thing that has made this supremely difficult is how much existing http frameworks on top of node.js base apis monkeypatch and work around the core API. It was impossible to get these working together without breaking existing stuff or making things far more complicated (and slow)
It's definitely possible but the flow is a bit difficult.
Can we get an example, maybe as a working test, for this? Once I know the flow I can get the right abstractions in place to make it easier.
Also consider that this is going to be even more difficult with the addition of quic/http3. There's no way to upgrade or failover since it's udp instead of tcp.
Ya, that’s fine, one problem at a time 😉 What is the plan for browsers in the regard?
Ok... so, first, you need to decide if your client is going to permit using plaintext HTTP/2. The spec says it's ok, browsers require it, and I wouldn't be surprised if most middleboxes that support it require it as a result.
Next, you need to decide: are you wanting to discover HTTP/2 support (and upgrade if available) or assume HTTP/2 support (and downgrade if not available).
The flows differ for each choice.
Plaintext HTTP/1 to HTTP/2 Upgrade (Discover):
Start by preparing an normal HTTP/1 request. GET or OPTIONS works best here. Include in the request an Upgrade
header specifying h2c
as the protocol, and an HTTP2-Settings
header that contains the Base64 encoded HTTP/2 SETTINGS
frame.
When that request is sent to the server, if the server supports HTTP/2, it should upgrade the connection just as it would any upgrade request. If you're familiar with the upgrade to WebSockets then you know what to expect here. Assuming the server upgrades the connection, it will apply the SETTINGS
in the HTTP2-Settings
header and will respond immediately with it's own SETTINGS
frame. This needs to be passed of to the HTTP/2 client handling code. I do not believe the current implementation in Node.js is set up to support this handoff yet but I could be mistaken.
Plaintext HTTP/2 to HTTP/1 Fallback:
You can choose to optimistically initiate an HTTP/2 request with the server. The initial HTTP/2 handshake is an intentionally malformed HTTP/1 method. If the server supports HTTP/2, then it will understand that you want to use HTTP/2 and will just continue working. If the server only supports HTTP/1, then it will respond with an HTTP/1 Unsupported Method error, if you get that, you need to fall back to using HTTP/1.
TLS HTTP/2 Negotiation:
If you are using TLS, the client uses the TLS ALPN extension to identify whether it wants to use HTTP/1 or HTTP/2. This is the easiest choice but the current implementation of the HTTP/1 and HTTP/2 APIs in Node.js would require you to first set up the TLS connection, establish which protocol you're using, then attach the TLS socket to either the HTTP/1 or HTTP/2 API. It's a bit painful.
ALTSVC:
With the Altsvc flow, any server that supports HTTP/2 (or in the future HTTP/3) can advertise that fact in the Altsvc header (or the Altsvc HTTP/2 extension frame). A client can send an initial HTTP/1 Options or GET request to solicit a response that contains the header, then use that to determine the location of the HTTP/2 server.
Ya, that’s fine, one problem at a time 😉 What is the plan for browsers in the regard?
Since all the browsers are Chromium now, and since Chromium is supporting QUIC, it'll just work. I'd need to check what their strategy for detecting availability is tho. Fairly certain they're going with the Altsvc option tho. Will try to verify.
There's this, but I don't know how up to date it is: https://www.chromium.org/quic/quic-faq ... use of Alternative-Protocol
definitely indicates that it is rather old.
There's also this in the HTTP/3 spec itself... essentially, Altsvc is the only currently standard discovery mechanism for QUIC, but that may change: https://tools.ietf.org/html/draft-ietf-quic-http-25#section-3.2
first, you need to decide if your client is going to permit using plaintext HTTP/2
nope, just TLS is fine.
How common is the ALTSVC support? Are we all that likely to encounter HTTP/2 servers that only support the other discovery method?
Fairly certain they're going with the Altsvc option tho. Will try to verify.
That seems like it would be the only option for supporting all 3.
then use that to determine the location of the HTTP/2 server.
With the Altsvc method are you not able to re-use the TLS connection when it’s used for HTTP/2?
It’s a bit much to open and then throw away a connection like this.
With the Altsvc method are you not able to re-use the TLS connection when it’s used for HTTP/2?
Yes, you can so long as you are absolutely certain that (a) the altsvc advertises the same port and (b) you're absolutely certain there's no other traffic going over that connection. (The HTTP/2 implementation needs to take complete control over the socket). I don't currently have stats on how common it is to share both implementations over a single port.
I would say that at this point in time, Altsvc and ALPN are the two most common approaches. Plaintext HTTP/2 is rare to find and likely not worth optimizing for.
I've been kicking around the idea of adding an Altsvc client implementation to core. Essentially something like:
const { getAltSvc } = require('http')
async function discover() {
const services = await getAltSvc('http://example.com')
if (services.h2) console.log('http2 available at ', services.h2)
if (services.h3) console.log('http3 available at ', services.h3)
}
(this can just as easily be an npm module.. I'd be shocked it if didn't exist already)
If the getAltSvc()
function is set up to take accept an existing Socket
, then you could easily configure that to be reused on the actual HTTP/1 or HTTP/2 connection.
(Oh, and to make things even more complicated down the road: there's been talk about making a DNS ALTSVC record and there's work ongoing to implement DNS-over-HTTP2 and DNS-over-QUIC 😄 )
Holy crap. Really?
Here's the alt-svc header returned when you send a request to google.com: alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
Notice that it's using the same port for each. All of these are QUIC/HTTP3 so you wouldn't be able to reuse the same connection.
Where reusing the same TLS connection would be problematic is when the implementation requires use of ALPN to specify the protocol that would be used on that connection. That is, if you create a TLS Connection on port 443 assuming HTTP/1 to get the Altsvc header, and it advertises HTTP/2 also on port 443, reusing the same connection may not work given that the connection was not started with the right ALPN identifier. It depends entirely on how the server side was implemented to behave and I'd be super surprised to find many that behave well.
I would use this module 😉
any movement on this? i’d like to find a way to add support for all this to bent
.
is there a module to get the alt-svc
header yet?
I've been working on a package that does ALPN negotiation (http2 / http1). We already have this in Got via the options.http2
option. It's off by default, because some people like to have their own super-customized Agents and that would be too breaking.
https://github.com/szmarczak/http2-wrapper#http2autourl-options-callback
I discussed this a while back with @jasnell and I forget where it all landed and what was needed.
I’d like to make it so that
bent
uses HTTP2 when available and falls back to HTTP 1.1 transparently when it’s not available.This should be possible, the standard was certainly designed that way, but I can’t find any examples of doing this in Node.js and the core http client libraries are totally separate. It could just be a documentation issue but I think there might actually be some missing API in order to make it work.