Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* 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/. */
// Test that when a sibling HE attempt wins the connection race, the
// claimed transaction on the losing attempt is requeued and completes.
"use strict";
var { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
const { NodeHTTP2Server } = ChromeUtils.importESModule(
);
let trrServer;
add_setup(async function () {
Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true);
Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6);
trrServer = new TRRServer();
await trrServer.start();
trr_test_setup();
Services.prefs.setIntPref("network.trr.mode", 3);
Services.prefs.setCharPref(
"network.trr.uri",
`https://foo.example.com:${trrServer.port()}/dns-query`
);
registerCleanupFunction(async () => {
Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled");
Services.prefs.clearUserPref("network.http.speculative-parallel-limit");
trr_clear_prefs();
if (trrServer) {
await trrServer.stop();
}
});
});
// Pause the first TCP connection at the socket level for delayMs.
// This fires before TLS, so it delays the handshake without corrupting it.
async function pauseFirstConnection(server, delayMs) {
await server.execute(`
global.firstConnPaused = false;
global.server.on("connection", (socket) => {
if (!global.firstConnPaused) {
global.firstConnPaused = true;
socket.pause();
setTimeout(() => { try { socket.resume(); } catch(e) {} }, ${delayMs});
}
});
`);
}
// Scenario:
// 1. Use TRR with a 500ms delay for the A record of alt1.example.com.
// This keeps the speculative HE attempt in DNS resolution phase.
// 2. A real channel arrives and Claim()s the speculative attempt while
// DNS is still pending. The NullTransaction is replaced with the real one.
// 3. DNS completes. MaybePassHttpTransToEstablisher sees the real transaction,
// creates a proxy, and removes the transaction from the pending queue.
// 4. The first TCP connection is paused at socket level for 5s, delaying
// its TLS handshake while subsequent connections complete immediately.
// 5. A second channel's HE attempt connects without delay, reports H2.
// MakeAllDontReuseExcept -> CloseAllConnectionAttempts abandons #1.
// 6. The proxy transaction is orphaned. Without the fix, it hangs.
add_task(async function test_requeue_on_sibling_abandon() {
Services.dns.clearCache(true);
Services.obs.notifyObservers(null, "net:cancel-all-connections");
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(resolve => setTimeout(resolve, 500));
let server = new NodeHTTP2Server();
await server.start();
await server.registerPathHandler("/test", (_req, resp) => {
resp.writeHead(200, { "Content-Type": "text/plain" });
resp.end("ok");
});
await pauseFirstConnection(server, 5000);
let serverPort = server.port();
let host = "alt1.example.com";
await trrServer.registerDoHAnswers(host, "A", {
answers: [
{ name: host, ttl: 55, type: "A", flush: false, data: "127.0.0.1" },
],
delay: 500,
});
let numChannels = 5;
let promises = [];
for (let i = 0; i < numChannels; i++) {
let chan = NetUtil.newChannel({
uri: `https://${host}:${serverPort}/test`,
loadUsingSystemPrincipal: true,
}).QueryInterface(Ci.nsIHttpChannel);
chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
let promise = new Promise(resolve => {
chan.asyncOpen({
onStartRequest() {},
onDataAvailable(req, stream, offset, count) {
read_stream(stream, count);
},
onStopRequest(req) {
let status = 0;
try {
status = req.QueryInterface(Ci.nsIHttpChannel).responseStatus;
} catch (e) {}
resolve({ status });
},
});
});
promises.push(promise);
}
let results = await Promise.all(promises);
let successCount = 0;
for (let i = 0; i < numChannels; i++) {
if (results[i].status === 200) {
successCount++;
}
}
Assert.equal(
successCount,
numChannels,
`All channels should succeed (got ${successCount}/${numChannels})`
);
await server.stop();
});