Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// Regression test for bug 2050384. An HTTP/3 connection that becomes unusable
// (e.g. DontReuse'd) but lingers in the entry's active connections must not
// keep the single-HTTP/3-per-entry slot: otherwise GetH2orH3ActiveConn skips
// it (so it can't serve the request) while AtActiveConnectionLimit still counts
// it (so no replacement is created), and the pending transaction is wedged
// forever.
//
// We reproduce the "unusable but still active" h3 connection with the
// network.http.http3.force_cannot_reuse_for_testing pref, which forces
// HttpConnectionUDP::CanReuse() to return false. With the fix a second request
// creates a fresh h3 connection and completes; without it the second request
// hangs.
"use strict";
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { NodeHTTP2Server } = ChromeUtils.importESModule(
);
const { HTTP3Server } = ChromeUtils.importESModule(
);
const override = Cc["@mozilla.org/network/native-dns-override;1"].getService(
Ci.nsINativeDNSResolverOverride
);
const mockController = Cc[
"@mozilla.org/network/mock-network-controller;1"
].getService(Ci.nsIMockNetworkLayerController);
let h3Port;
let h3Server;
let h2Server;
let h3ServerPath;
let h3DBPath;
async function startH3Server() {
h3Server = new HTTP3Server();
await h3Server.start(h3ServerPath, h3DBPath);
h3Port = h3Server.port();
}
async function stopH3Server() {
if (h3Server) {
await h3Server.stop();
h3Server = null;
}
}
add_setup(async function () {
h3ServerPath = Services.env.get("MOZ_HTTP3_SERVER_PATH");
h3DBPath = Services.env.get("MOZ_HTTP3_CERT_DB_PATH");
let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
Ci.nsIX509CertDB
);
addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setBoolPref("network.http.http3.enable", true);
Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true);
// Avoid speculative connections adding extra attempts to the entry.
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0);
h2Server = new NodeHTTP2Server();
await h2Server.start();
await h2Server.registerPathHandler("/", (_req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ok");
});
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.http3.enable");
Services.prefs.clearUserPref("network.socket.attach_mock_network_layer");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
Services.prefs.clearUserPref(
"network.http.http3.alt-svc-mapping-for-testing"
);
Services.prefs.clearUserPref(
"network.http.http3.force_cannot_reuse_for_testing"
);
override.clearOverrides();
mockController.clearBlockedTCPConnect();
if (h2Server) {
await h2Server.stop();
}
await stopH3Server();
});
});
async function resetConnections() {
Services.obs.notifyObservers(null, "net:cancel-all-connections");
Services.obs.notifyObservers(null, "browser:purge-session-history");
let nssComponent = Cc["@mozilla.org/network/ssl-tokens-cache;1"].getService(
Ci.nsISSLTokensCache
);
await nssComponent.asyncClearSSLExternalAndInternalSessionCache();
Services.dns.clearCache(true);
override.clearOverrides();
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 1000));
}
async function openChan(uri) {
let chan = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
let result = await new Promise(resolve => {
chan.asyncOpen(
new ChannelListener(
(r, b) => resolve({ req: r, buffer: b }),
null,
CL_ALLOW_UNKNOWN_CL
)
);
});
return {
httpVersion: result.req.protocolVersion,
status: result.req.QueryInterface(Ci.nsIHttpChannel).responseStatus,
buffer: result.buffer,
};
}
add_task(async function test_unusable_h3_conn_does_not_wedge_entry() {
await startH3Server();
await resetConnections();
mockController.clearBlockedTCPConnect();
let host = "foo.example.com";
override.addIPOverride(host, "127.0.0.1");
Services.prefs.setCharPref(
"network.http.http3.alt-svc-mapping-for-testing",
`${host};h3=:${h3Port}`
);
// Block TCP so the H2 fallback cannot win the race against H3, i.e. the
// request is served over an HTTP/3 connection.
let blockedTCP = mockController.createScriptableNetAddr(
"127.0.0.1",
h2Server.port()
);
mockController.blockTCPConnect(blockedTCP);
// First request establishes an active HTTP/3 connection on the entry.
let first = await openChan(`https://${host}:${h2Server.port()}/`);
Assert.equal(first.status, 200, "First request should succeed");
Assert.equal(first.httpVersion, "h3", "First request should use HTTP/3");
// Make the established HTTP/3 connection unusable while it stays in the
// entry's active connections (mirroring a DontReuse'd connection that hasn't
// been reaped).
Services.prefs.setBoolPref(
"network.http.http3.force_cannot_reuse_for_testing",
true
);
// The second request cannot reuse the now-unusable connection, so it must be
// able to create a fresh HTTP/3 connection. Before the fix it hangs, because
// the unusable connection still counts against the single-HTTP/3-per-entry
// limit and blocks a replacement.
let second = await openChan(`https://${host}:${h2Server.port()}/`);
Assert.equal(
second.status,
200,
"Second request should succeed on a new HTTP/3 connection"
);
Assert.equal(second.httpVersion, "h3", "Second request should use HTTP/3");
Services.prefs.clearUserPref(
"network.http.http3.force_cannot_reuse_for_testing"
);
mockController.clearBlockedTCPConnect();
await stopH3Server();
});