caddy: Firefox + HTTP/3 + Streaming Responses = Promises That Don’t Resolve
Hi everyone,
I’m talking to you about this because I ran into this issue when I updated to Caddy 2.6.1, but maybe the issue is actually in Firefox 🤷
In any event, here’s what you need to reproduce:
$ npm install @leafac/caddy express
# Caddyfile
localhost
reverse_proxy localhost:4000
// index.mjs
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send(`fetch("/stream").then(() => {console.log("PROMISE RESOLVED")})`);
});
app.get("/stream", (req, res) => {
let heartbeatTimeout;
(function heartbeat() {
res.write("\n");
heartbeatTimeout = setTimeout(heartbeat, 15 * 1000);
})();
res.once("close", () => {
clearTimeout(heartbeatTimeout);
});
});
app.listen(4000);
Note how the server has two routes: /, which is just a “hello world”, and /stream, which streams a heartbeat (a new line) every 15 seconds while the connection is kept alive.
You may run this with:
$ node index.mjs
$ npx @leafac/caddy run
Now go to Firefox (for what it’s worth, I’m on 105.0.1 in macOS), and try and get Firefox to load https://localhost with HTTP/3. You’ll know you succeed because the developer tools will show you the following:
I’m not sure yet what makes browsers prefer HTTP/3 over HTTP/2, but here are some of the things that worked for me:
- “Disable Cache” under the Network tab in the developer tools.
- Restart the browser.
- Reload a bunch of times.
- Repeat…
If y’all know of a better way, please let me know!
Anyway, when you finally get it, go to the Console in the developer tools and run the command shown to you on the page:
fetch("/stream").then(() => {console.log("PROMISE RESOLVED")})
Note how the promise never gets resolved!
What’s more, sometimes the requests don’t even show in the Network tab. Firefox really stopped playing ball…
Now, to show that this seems to be related to HTTP/3, change the Caddyfile to the following:
# Caddyfile
{
servers {
protocols h1 h2
}
}
localhost
reverse_proxy localhost:4000
Note how we’re not listing HTTP/3 among the protocols.
Restart Caddy.
This time Firefox will make the request and resolve the promise every time:
Any insight is much appreciated, even if it’s “Caddy is doing everything right, go talk to the people at Firefox about it” 😁
Thank you very much!
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 28 (19 by maintainers)
Nice, https://github.com/quic-go/quic-go/releases/tag/v0.33.0 includes the fix for this I think, via https://github.com/quic-go/quic-go/pull/3715. It’ll be included in Caddy’s next release as of https://github.com/caddyserver/caddy/commit/8cb1bb4af30b880f981b1ae6fdb13e5944bfaefe. If anyone would like to test it by building from
master, that’d be great!@marten-seemann That sounds like a fun challenge! 👏
I have only a little bit of experience with Go and the codebase for quic-go seems a bit scary at first sight. Can you give me a couple pointers to get started?
@leafac That makes sense. I don’t think we should block. Do you want to contribute a fix to quic-go? 😃
Thank you @mholt! I guess that means we’d have to block until the first 512 bytes of the response body are written, right?
I’m super busy at the moment, but I’d be happy to review a PR, if anyone wants to submit one.
@mholt @francislavoie quic-go doesn’t do any sniffing. Can you point me to the code where exactly the std lib does that?
I’d definitely be open to adding something like this to quic-go, if it maintains parity between H3 and older HTTP versions.
Hmmm 🤔 I think it’s a bug, but I don’t know if they’ll agree 🤷
Perhaps it’s their preference that a
fetch()promise only resolves after content sniffing received enough bytes. Streaming responses better send aContent-Typein that case…Or perhaps they’ll prefer to do what Google Chrome does…
@mholt:
Yes!
With the configuration that you provided I can reproduce the issue: Google Chrome in HTTP/3 mode doesn’t receive the
Content-Typeheader, andcurl -sv https://localhost > /dev/nullin HTTP/2 mode does receive it:Thanks for the help! 😁
Interesting, our handlers are the same no matter the http version. I wonder if there is a difference between what the quic-go server does from what the std lib server does.
I tried
flush_interval -1and, as we expected, it didn’t change Firefox’s behavior.For completeness, here’s the
```caddyfile # Caddyfile localhost reverse_proxy localhost:4000 { flush_interval -1 } ```CaddyfileI then found https://everything.curl.dev/get/macos, by the author of curl, with recommends getting curl from Homebrew for the latest version. As it turns out, Homebrew’s version of curl is the latest, but doesn’t support HTTP/3 😐
I then found Cloudflare’s formula for curl which is supposed to have HTTP/3 support. I tried to install it but ran into all sorts of compilation problems.
The instructions at https://curl.se/docs/http3.html look a bit too involved, so I decided not to pursue this route right now.
At this point I gave up on curl for now and decided to chalk up the problem as an issue in Firefox.
I’m closing this issue in favor of https://bugzilla.mozilla.org/show_bug.cgi?id=1793342
Thank you very much for your help!