fix(lighthouse): explain unsupported URL schemes in navigation mode (#3144)

Closes #2969

In `navigation` mode, Lighthouse only accepts http:, https:, chrome: and
chrome-extension: URLs. For anything else (file://, about:blank) it
throws a bare `INVALID_URL`, which doesn't say what's wrong or that
`mode: "snapshot"` works on the same page.

This catches Lighthouse's `INVALID_URL` error in navigation mode and
rethrows it with a message that names the URL, lists the supported
schemes and suggests `mode: "snapshot"`. The original error is kept as
`cause`. Other errors pass through unchanged.

I catch the error rather than checking the scheme up front, so the
message can't drift from Lighthouse's own `allowedProtocols` list.

Tests (mocked):
- navigation mode rejecting with INVALID_URL on a file:// URL → the new
message, with the original error as cause (fails on main)
- other navigation errors → passed through unchanged
I left the `mode` parameter description as it is. Happy to add a note
about file:// there too (and regenerate the docs) if you'd like.
This commit is contained in:
nancy authored and GitHub committed 2026-10-16 09:24:21 +00:00
1 parent 91e112d3dd
commit c6ad84a23b
2 files changed
+72 -6

No files matched your search

+20 -6
View File
@@ -44,6 +44,13 @@ const isRecord = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object' && value !== null;
};
// Lighthouse's navigation mode only accepts the URL schemes in
// `allowedProtocols` (core/lib/url-utils.js) and throws a bare `INVALID_URL`
// for anything else, such as file:// or about:blank.
const isInvalidUrlError = (error: unknown): boolean => {
return isRecord(error) && error.code === 'INVALID_URL';
};
const getString = (value: unknown): string | undefined => {
return typeof value === 'string' ? value : undefined;
};
@@ -233,13 +240,20 @@ export const lighthouseAudit = definePageTool((args: ParsedArguments) => ({
let result: RunnerResult | undefined;
try {
if (mode === 'navigation') {
result = await lighthouseRunner.navigation(
page.pptrPage,
page.pptrPage.url(),
{
const url = page.pptrPage.url();
try {
result = await lighthouseRunner.navigation(page.pptrPage, url, {
flags,
},
);
});
} catch (error) {
if (isInvalidUrlError(error)) {
throw new Error(
`Lighthouse "navigation" mode cannot audit ${url}: it only supports http:, https:, chrome: and chrome-extension: URLs. Use mode "snapshot" to audit the page in its current state.`,
{cause: error},
);
}
throw error;
}
} else {
result = await lighthouseRunner.snapshot(page.pptrPage, {
flags,
+52
View File
@@ -60,6 +60,58 @@ describe('lighthouse', () => {
});
});
it('explains which URLs navigation mode can audit', async () => {
const {page, context, response, args} = createHandlerMocks();
page.pptrPage.url.returns('file:///tmp/page.html');
const invalidUrl = Object.assign(new Error('INVALID_URL'), {
code: 'INVALID_URL',
});
sinon.stub(lighthouseRunner, 'navigation').rejects(invalidUrl);
await assert.rejects(
() =>
lighthouseAudit(args).handler(
{
params: {
mode: 'navigation',
device: 'desktop',
},
page,
},
response,
context,
),
{
message:
'Lighthouse "navigation" mode cannot audit file:///tmp/page.html: it only supports http:, https:, chrome: and chrome-extension: URLs. Use mode "snapshot" to audit the page in its current state.',
cause: invalidUrl,
},
);
});
it('passes other navigation errors through unchanged', async () => {
const {page, context, response, args} = createHandlerMocks();
sinon
.stub(lighthouseRunner, 'navigation')
.rejects(new Error('PROTOCOL_TIMEOUT'));
await assert.rejects(
() =>
lighthouseAudit(args).handler(
{
params: {
mode: 'navigation',
device: 'desktop',
},
page,
},
response,
context,
),
{message: 'PROTOCOL_TIMEOUT'},
);
});
it('restores emulation', async () => {
const {page, context, response, args} = createHandlerMocks();
context.saveTemporaryFile.resolves({filepath: 'report.json'});