Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'android' && os_version == '14' && arch == 'x86_64' OR os == 'win' && os_version == '11.26100' && arch == 'x86_64' && msix OR os == 'win' && os_version == '11.26200' && arch == 'x86_64' && msix
- Manifest: netwerk/test/unit/xpcshell.toml
"use strict";
// validated so the h3 connection is created eagerly and a later request reuses
// it. Unlike test_http3_alt_svc.js, this test never drops connections between
// requests (which would force the h3 route and hide the regression): after the
// h2 response advertises the h3 route, a follow-up must be served over h3.
// The origin is a Node HTTP/2 server; the alternate is the Rust HTTP/3 server.
const { NodeHTTP2Server, HTTP3Server } = ChromeUtils.importESModule(
);
const { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
let h2Server;
let h3Server;
let httpsOrigin;
let h3Route;
let h3AltSvc;
add_setup(async function () {
let h3ServerPath = Services.env.get("MOZ_HTTP3_SERVER_PATH");
let h3DBPath = Services.env.get("MOZ_HTTP3_CERT_DB_PATH");
do_get_profile();
Services.prefs.setBoolPref("network.http.http3.enable", true);
// The scenario this test guards only reproduces with Happy Eyeballs enabled:
// that is what makes the alternate share the origin's ConnectionEntry and, in
// the buggy state, skips validation.
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
// The h3 alternate: same host (foo.example.com), different port.
h3Server = new HTTP3Server();
await h3Server.start(h3ServerPath, h3DBPath);
let h3Port = h3Server.port();
h3Route = "foo.example.com:" + h3Port;
h3AltSvc = ":" + h3Port;
// The origin advertises the h3 alternate. The handler runs in the node
// process (serialized via toString(), so no closures), so the alternate is
// passed via the x-altsvc request header and echoed into Alt-Svc.
h2Server = new NodeHTTP2Server();
await h2Server.start();
await h2Server.registerPathHandler("/http3-test", (req, resp) => {
resp.writeHead(200, {
"Content-Type": "text/plain",
"Alt-Svc": "h3=" + req.headers["x-altsvc"],
});
resp.end("a".repeat(100));
});
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.http3.enable");
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.dns.localDomains");
Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
if (h2Server) {
await h2Server.stop();
}
if (h3Server) {
await h3Server.stop();
}
});
});
function makeChan(uri) {
let chan = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
return chan;
}
function channelProtocol(request) {
try {
return request.protocolVersion;
} catch (e) {
return "";
}
}
function channelRoute(request) {
try {
return request.getRequestHeader("Alt-Used");
} catch (e) {
return "";
}
}
// Opens `uri` and resolves with the request's protocol version and Alt-Used
// route.
function requestOnce(uri) {
return new Promise(resolve => {
let chan = makeChan(uri);
chan.setRequestHeader("x-altsvc", h3AltSvc, false);
chan.asyncOpen({
QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
onStartRequest(request) {
Assert.equal(
request.QueryInterface(Ci.nsIHttpChannel).responseStatus,
200
);
},
onDataAvailable(request, stream, off, cnt) {
read_stream(stream, cnt);
},
onStopRequest(request, status) {
Assert.ok(Components.isSuccessCode(status));
resolve({
protocol: channelProtocol(request),
route: channelRoute(request),
});
},
});
});
}
const MAX_POLL_RETRIES = 50;
function waitMs(ms) {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
return new Promise(resolve => setTimeout(resolve, ms));
}
// Poll the origin (without ever dropping connections) until it is served over
// h3, i.e. the eagerly-validated h3 connection is being reused.
async function pollUntilH3(uri) {
for (let i = 0; i < MAX_POLL_RETRIES; i++) {
let { protocol, route } = await requestOnce(uri);
info(`follow-up #${i}: protocol=${protocol} route=${route}`);
if (protocol == "h3") {
Assert.equal(route, h3Route, "request routed over the alt-svc h3 route");
return true;
}
await waitMs(500);
}
return false;
}
add_task(async function test_altsvc_validation_creates_reusable_h3() {
let uri = httpsOrigin + "http3-test";
// First request establishes the origin connection over h2 and receives the
// Alt-Svc: h3 header, which (post-fix) triggers eager validation of the h3
// route.
let first = await requestOnce(uri);
info(`first request: protocol=${first.protocol} route=${first.route}`);
Assert.equal(first.protocol, "h2", "origin request is served over h2");
// Without tearing down any connection, a follow-up request must end up on h3
// because validation created a reusable h3 connection.
let usedH3 = await pollUntilH3(uri);
Assert.ok(
usedH3,
"a follow-up request reused the eagerly-validated h3 connection"
);
});