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/. */
"use strict";
Services.prefs.setBoolPref("network.early-hints.enabled", true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref("network.early-hints.enabled");
});
// This function tests Early Hint responses before and in between HTTP redirects.
//
// Arguments:
// - name: String identifying the test case for easier parsing in the log
// - chain and destination: defines the redirect chain, see example below
// note: ALL preloaded urls must be image urls
// - expected: number of normal, cancelled and completed hinted responses.
//
// # Example
// The parameter values of
// ```
// chain = [
// {link:"https://link1", host:"https://host1.com"},
// {link:"https://link2", host:"https://host2.com"},
// ]
// ```
// and `destination = "https://host3.com/page.html" would result in the
// following HTTP exchange (simplified):
//
// ```
//
// < 103 Early Hints
// < Link: <https://link1>;rel=preload;as=image
// <
// < 307 Temporary Redirect
// <
//
//
// < 103 Early Hints
// < Link: <https://link2>;rel=preload;as=image
// <
// < 307 Temporary Redirect
// <
//
//
// < [...] Result depends on the final page
// ```
//
// Legend:
// * `>` indicates a request going from client to server
// * `<` indicates a response going from server to client
// * all lines are terminated with a `\r\n`
//
async function test_hint_redirect(
name,
chain,
destination,
hint_destination,
expected
) {
// pass the full redirect chain as a url parameter. Each redirect is handled
// by `early_hint_redirect_html.sjs` which url-decodes the query string and
// redirects to the result
let links = [];
let url = destination;
for (let i = chain.length - 1; i >= 0; i--) {
let qp = new URLSearchParams();
if (chain[i].link != "") {
qp.append("link", "<" + chain[i].link + ">;rel=preload;as=image");
links.push(chain[i].link);
}
qp.append("location", url);
url = `${
chain[i].host
}/browser/netwerk/test/browser/early_hint_redirect_html.sjs?${qp.toString()}`;
}
if (hint_destination != "") {
links.push(hint_destination);
}
// reset the count
let headers = new Headers();
headers.append("X-Early-Hint-Count-Start", "");
await fetch(
{ headers }
);
// main request and all other must get their respective OnStopRequest
let numRequestRemaining =
expected.normal + expected.hinted + expected.cancelled;
let observed = {
hinted: 0,
normal: 0,
cancelled: 0,
};
// store channelIds
let observedChannelIds = [];
let callback;
let promise = new Promise(resolve => {
callback = resolve;
});
if (numRequestRemaining > 0) {
let observer = {
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
observe(aSubject, aTopic) {
aSubject.QueryInterface(Ci.nsIIdentChannel);
let id = aSubject.channelId;
if (observedChannelIds.includes(id)) {
return;
}
aSubject.QueryInterface(Ci.nsIRequest);
dump("Observer aSubject.name " + aSubject.name + "\n");
if (aTopic == "http-on-stop-request" && links.includes(aSubject.name)) {
if (aSubject.status == Cr.NS_ERROR_ABORT) {
observed.cancelled += 1;
} else {
aSubject.QueryInterface(Ci.nsIHttpChannel);
let initiator = "";
try {
initiator = aSubject.getRequestHeader("X-Moz");
} catch {}
if (initiator == "early hint") {
observed.hinted += 1;
} else {
observed.normal += 1;
}
}
observedChannelIds.push(id);
numRequestRemaining -= 1;
dump("Observer numRequestRemaining " + numRequestRemaining + "\n");
}
if (numRequestRemaining == 0) {
Services.obs.removeObserver(observer, "http-on-stop-request");
callback();
}
},
};
Services.obs.addObserver(observer, "http-on-stop-request");
} else {
callback();
}
await BrowserTestUtils.withNewTab(
{
gBrowser,
url,
waitForLoad: true,
},
async function () {}
);
// wait until all requests are stopped, especially the cancelled ones
await promise;
let got = await fetch(
).then(response => response.json());
// stringify to pretty print assert output
let g = JSON.stringify(observed);
let e = JSON.stringify(expected);
Assert.equal(
expected.normal,
observed.normal,
`${name} normal observed from client expected ${expected.normal} (${e}) got ${observed.normal} (${g})`
);
Assert.equal(
expected.hinted,
observed.hinted,
`${name} hinted observed from client expected ${expected.hinted} (${e}) got ${observed.hinted} (${g})`
);
Assert.equal(
expected.cancelled,
observed.cancelled,
`${name} cancelled observed from client expected ${expected.cancelled} (${e}) got ${observed.cancelled} (${g})`
);
// each cancelled request might be cancelled after the request was already
// made. Allow cancelled responses to count towards the hinted to avoid
// intermittent test failures.
Assert.ok(
expected.hinted <= got.hinted &&
got.hinted <= expected.hinted + expected.cancelled,
`${name}: unexpected amount of hinted request made got ${
got.hinted
}, expected between ${expected.hinted} and ${
expected.hinted + expected.cancelled
}`
);
Assert.ok(
got.normal == expected.normal,
`${name}: unexpected amount of normal request made expected ${expected.normal}, got ${got.normal}`
);
Assert.equal(numRequestRemaining, 0, "Requests remaining");
}
add_task(async function double_redirect_cross_origin() {
await test_hint_redirect(
"double_redirect_cross_origin_both_hints",
[
{
},
{
},
],
{ hinted: 1, normal: 0, cancelled: 2 }
);
await test_hint_redirect(
"double_redirect_second_hint",
[
{
link: "",
},
{
},
],
{ hinted: 1, normal: 0, cancelled: 1 }
);
await test_hint_redirect(
"double_redirect_first_hint",
[
{
},
{
},
],
{ hinted: 0, normal: 1, cancelled: 2 }
);
});
add_task(async function redirect_cross_origin() {
await test_hint_redirect(
"redirect_cross_origin_start_second_preload",
[
{
},
],
{ hinted: 1, normal: 0, cancelled: 1 }
);
await test_hint_redirect(
"redirect_cross_origin_dont_use_first_preload",
[
{
},
],
{ hinted: 0, normal: 1, cancelled: 1 }
);
});
add_task(async function redirect_same_origin() {
await test_hint_redirect(
"hint_before_redirect_same_origin",
[
{
},
],
{ hinted: 1, normal: 0, cancelled: 0 }
);
await test_hint_redirect(
"hint_after_redirect_same_origin",
[
{
},
],
{ hinted: 1, normal: 0, cancelled: 0 }
);
await test_hint_redirect(
"hint_after_redirect_same_origin",
[
{
link: "",
},
],
{ hinted: 1, normal: 0, cancelled: 0 }
);
});