Create a key pair.
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem
Run the server.
node index.js
Open 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.pem
Run the server.
node index.js
Open 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.