Add timeout to HiddenBrowser::getPageData()

The queries wait for the document to be ready, so a page that never
finished loading would hang the caller -- e.g., full-text indexing --
forever.
This commit is contained in:
Dan Stillman 2026-07-24 11:06:28 -04:00
parent 9eea7d9a0c
commit 512a2444d8
2 changed files with 61 additions and 2 deletions

View file

@ -266,12 +266,22 @@ export class HiddenBrowser {
/**
* @param {String[]} props - 'characterSet', 'title', 'bodyText', 'documentHTML', 'cookie', 'channelInfo'
* @param {Object} [options]
* @param {Number} [options.timeout=30000] - Time to wait for each property in milliseconds.
* The queries wait for the document to be ready, so a page that never finishes loading
* would otherwise hang the query forever.
*/
async getPageData(props) {
async getPageData(props, { timeout = 30000 } = {}) {
var actor = this.browsingContext.currentWindowGlobal.getActor("PageData");
var data = {};
for (let prop of props) {
data[prop] = await actor.sendQuery(prop);
let timeoutPromise = new Promise((_, reject) => {
setTimeout(
() => reject(new Error(`Timed out getting '${prop}' from hidden browser`)),
timeout
);
});
data[prop] = await Promise.race([actor.sendQuery(prop), timeoutPromise]);
}
return data;
}

View file

@ -182,6 +182,55 @@ describe("HiddenBrowser", function() {
});
});
describe("#getPageData() timeout", function () {
var httpd;
var port = 16216;
var heldResponses = [];
before(function () {
httpd = new HttpServer();
httpd.start(port);
// Same-origin page whose parser-blocking script never finishes loading, so the
// document never reaches 'interactive'
httpd.registerPathHandler('/hangpage', {
handle(request, response) {
response.setHeader('Content-Type', 'text/html', false);
response.setStatusLine(null, 200, 'OK');
response.write('<html><head><script src="/hang.js"></script></head>'
+ '<body>x</body></html>');
}
});
httpd.registerPathHandler('/hang.js', {
handle(request, response) {
response.processAsync();
response.setStatusLine(null, 200, 'OK');
response.setHeader('Content-Type', 'text/javascript', false);
heldResponses.push(response);
}
});
});
after(async function () {
for (let r of heldResponses) {
try { r.finish(); } catch (e) {}
}
await new Promise(resolve => httpd.stop(resolve));
});
it("getPageData should reject instead of hanging on a never-ready document", async function () {
this.timeout(20000);
let browser = new HiddenBrowser();
await browser.load(`http://127.0.0.1:${port}/hangpage`);
let start = Date.now();
let e = await getPromiseError(browser.getPageData(['bodyText'], { timeout: 2000 }));
let elapsed = Date.now() - start;
browser.destroy();
assert.ok(e, 'getPageData rejected');
assert.include(e.message, 'Timed out');
assert.isBelow(elapsed, 10000, 'rejected around the timeout, not after a long hang');
});
});
describe("#getDocument()", function () {
it("should provide a Document object", async function () {
let path = OS.Path.join(getTestDataDirectory().path, 'test-hidden.html');