Create a key pair.
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pemRun the server.
node index.jsOpen https://localhost:8443/ with Firefox.
Create a key pair.
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pemRun the server.
node index.jsOpen https://localhost:8443/ with Firefox.
| const http2 = require("http2"); | |
| const { HTTP2_HEADER_PATH } = http2.constants; | |
| const fs = require("fs"); | |
| const SCRIPT = ` | |
| function fetchAndSendBeacon(button, beaconPath) { | |
| fetch("/slow").then(() => { | |
| button.textContent += ": done"; | |
| }, () => { | |
| button.textContent += ": error"; | |
| }); | |
| setTimeout(() => { | |
| navigator.sendBeacon(beaconPath, "{}"); | |
| }, 0); | |
| } | |
| document.getElementById("without-body").addEventListener("click", (e) => { | |
| fetchAndSendBeacon(e.currentTarget, "/beacon-without-body"); | |
| }); | |
| document.getElementById("with-body").addEventListener("click", (e) => { | |
| fetchAndSendBeacon(e.currentTarget, "/beacon-with-body"); | |
| }); | |
| document.getElementById("with-body-and-delay").addEventListener("click", (e) => { | |
| fetchAndSendBeacon(e.currentTarget, "/beacon-with-body-and-delay"); | |
| }); | |
| document.getElementById("with-double-body").addEventListener("click", (e) => { | |
| fetchAndSendBeacon(e.currentTarget, "/beacon-with-double-body"); | |
| }); | |
| `; | |
| const PAGE = `<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf8"> | |
| </head> | |
| <body> | |
| <button id="without-body">Without Body</button> | |
| <button id="with-body">With Body</button> | |
| <button id="with-body-and-delay">With Body and Delay</button> | |
| <button id="with-double-body">With Double Body</button> | |
| <script>${SCRIPT}</script> | |
| </body> | |
| </html> | |
| `; | |
| function log(...args) { | |
| console.log(`\u001b[32m[${new Date().toISOString()}]\u001b[0m`, ...args); | |
| } | |
| const server = http2.createSecureServer({ | |
| key: fs.readFileSync("localhost-privkey.pem"), | |
| cert: fs.readFileSync("localhost-cert.pem") | |
| }); | |
| server.on("error", err => { | |
| log("server error", err); | |
| }); | |
| server.on("session", session => { | |
| log("session created"); | |
| session.on("error", err => { | |
| log("session error:", err); | |
| }); | |
| session.on("stream", (stream, headers) => { | |
| stream.on("error", err => { | |
| log("stream error:", err); | |
| }); | |
| switch (headers[HTTP2_HEADER_PATH]) { | |
| case "/": | |
| stream.respond({ | |
| "content-type": "text/html", | |
| ":status": 200 | |
| }); | |
| stream.end(PAGE); | |
| break; | |
| case "/beacon-without-body": | |
| stream.respond({ | |
| "content-type": "text/plain", | |
| ":status": 200 | |
| }); | |
| stream.end(); | |
| break; | |
| case "/beacon-with-body": { | |
| const body = "Hello World"; | |
| stream.respond({ | |
| "content-type": "text/plain", | |
| "content-length": body.length, | |
| ":status": 200 | |
| }); | |
| stream.end(body); | |
| break; | |
| } | |
| case "/beacon-with-body-and-delay": { | |
| const body = "Hello World"; | |
| stream.respond({ | |
| "content-type": "text/plain", | |
| "content-length": body.length, | |
| ":status": 200 | |
| }); | |
| stream.write(body); | |
| // Firefox sends GOAWAY if this delay exists. | |
| setTimeout(() => { | |
| stream.end(); | |
| }, 10); | |
| break; | |
| } | |
| case "/beacon-with-double-body": { | |
| const body = "Hello World"; | |
| stream.respond({ | |
| "content-type": "text/plain", | |
| "content-length": body.length * 2, | |
| ":status": 200 | |
| }); | |
| stream.write(body); | |
| setTimeout(() => { | |
| if (!stream.closed) { | |
| stream.end(body); | |
| } | |
| }, 10); | |
| break; | |
| } | |
| case "/slow": | |
| setTimeout(() => { | |
| if (!stream.closed) { | |
| stream.respond({ | |
| "content-type": "text/plain", | |
| ":status": 200 | |
| }); | |
| stream.end("Hello World"); | |
| } | |
| }, 500); | |
| break; | |
| default: | |
| stream.respond({ | |
| "content-type": "text/html", | |
| ":status": 401 | |
| }); | |
| stream.end("<h1>Not found</h1>"); | |
| } | |
| }); | |
| }); | |
| server.listen(8443); | |
Code that can set NS_ERROR_NET_INTERRUPT (from grep):
dom/base/Navigator.cpp:1050: aRequest->Cancel(NS_ERROR_NET_INTERRUPT);`
netwerk/base/nsSocketTransport2.cpp:155: rv = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http2Session.cpp:3390: : NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/Http3Session.cpp:671: CloseStream(stream, NS_ERROR_NET_INTERRUPT);
netwerk/protocol/http/nsHttpChannel.cpp:8372: aStatus = NS_ERROR_NET_INTERRUPT;
netwerk/protocol/http/nsHttpTransaction.cpp:1631: return NS_ERROR_NET_INTERRUPT;
Not relevant:
netwerk/protocol/http/Http2Session.cpp:3390: This should be called after RST_STREAM, but RST_STREAM is not captured in Wireshark when GOAWAY happens.netwerk/protocol/http/Http3Session.cpp:671: This issue is about HTTP2 instead of HTTP3.netwerk/protocol/http/nsHttpChannel.cpp:8372: This line is about Range request failure, but the beacon is not using Range header.Relevant?:
dom/base/Navigator.cpp:1050: The initial ticket for navigator.sendBeacon has some discussions around it. https://bugzilla.mozilla.org/show_bug.cgi?id=936340netwerk/base/nsSocketTransport2.cpp:155: PR_END_OF_FILE_ERROR from NSPR (Netscape Portable Runtime) is translated to NS_ERROR_NET_INTERRUPT. It seems that PR_END_OF_FILE_ERROR can come from TLS-related low-level functions. This may not be relevant.netwerk/protocol/http/nsHttpTransaction.cpp:1631: A comment suggests that it's for 100 class response, but not sure.It was fixed in Nightly! https://bugzilla.mozilla.org/show_bug.cgi?id=1613943
Also, if it doesn't send goaway if the content-length is not set in the response body.