diff --git a/lighthouse-cli/test/smokehouse/error-expectations.js b/lighthouse-cli/test/smokehouse/error-expectations.js index 0d2e40ac47eb..7f0f9bc80401 100644 --- a/lighthouse-cli/test/smokehouse/error-expectations.js +++ b/lighthouse-cli/test/smokehouse/error-expectations.js @@ -27,10 +27,10 @@ module.exports = [ finalUrl: 'https://expired.badssl.com/', audits: {}, // TODO: runtimeError only exists because of selection of audits. - runtimeError: {code: 'FAILED_DOCUMENT_REQUEST'}, + runtimeError: {code: 'INSECURE_DOCUMENT_REQUEST'}, }, artifacts: { - ViewportDimensions: {code: 'FAILED_DOCUMENT_REQUEST'}, + ViewportDimensions: {code: 'INSECURE_DOCUMENT_REQUEST'}, }, }, ]; diff --git a/lighthouse-core/gather/gather-runner.js b/lighthouse-core/gather/gather-runner.js index 99fc7b6ffd5e..09003ef4ae99 100644 --- a/lighthouse-core/gather/gather-runner.js +++ b/lighthouse-core/gather/gather-runner.js @@ -167,6 +167,37 @@ class GatherRunner { } } + /** + * Returns an error if we ended up on the `chrome-error` page and all other requests failed. + * @param {Array} networkRecords + * @return {LH.LighthouseError|undefined} + */ + static getInterstitialError(networkRecords) { + const interstitialRequest = networkRecords + .find(record => record.documentURL.startsWith('chrome-error://')); + // If the page didn't end up on a chrome interstitial, there's no error here. + if (!interstitialRequest) return undefined; + + const pageNetworkRecords = networkRecords + .filter(record => !URL.NON_NETWORK_PROTOCOLS.includes(record.protocol) && + !record.documentURL.startsWith('chrome-error://')); + // If none of the requests failed, there's no error here. + // We don't expect that this case could ever occur, but better safe than sorry. + // Note also that in cases of redirects, the initial requests could succeed and we still end up + // on the error interstitial page. + if (!pageNetworkRecords.some(record => record.failed)) return undefined; + + // If a request failed with the `net::ERR_CERT_*` collection of errors, then it's a security issue. + const insecureRequest = pageNetworkRecords.find(record => + record.failed && record.localizedFailDescription.startsWith('net::ERR_CERT')); + if (insecureRequest) { + return new LHError(LHError.errors.INSECURE_DOCUMENT_REQUEST, {securityMessages: + insecureRequest.localizedFailDescription}); + } + + return new LHError(LHError.errors.CHROME_INTERSTITIAL_ERROR); + } + /** * Returns an error if the page load should be considered failed, e.g. from a * main document request failure, a security issue, etc. @@ -177,10 +208,14 @@ class GatherRunner { */ static getPageLoadError(passContext, loadData, navigationError) { const networkError = GatherRunner.getNetworkError(passContext.url, loadData.networkRecords); + const interstitialError = GatherRunner.getInterstitialError(loadData.networkRecords); - // If the driver was offline, the load will fail without offline support. Ignore this case. + // If the driver was offline, the load will fail without offline support. Ignore this case. if (!passContext.driver.online) return; + // We want to special-case the interstitial beyond FAILED_DOCUMENT_REQUEST. See https://github.com/GoogleChrome/lighthouse/pull/8865#issuecomment-497507618 + if (interstitialError) return interstitialError; + // Network errors are usually the most specific and provide the best reason for why the page failed to load. // Prefer networkError over navigationError. // Example: `DNS_FAILURE` is better than `NO_FCP`. diff --git a/lighthouse-core/lib/i18n/en-US.json b/lighthouse-core/lib/i18n/en-US.json index adfc34c8fa21..db52d10c0f8a 100644 --- a/lighthouse-core/lib/i18n/en-US.json +++ b/lighthouse-core/lib/i18n/en-US.json @@ -1347,6 +1347,10 @@ "message": "The URL you have provided does not have a valid security certificate. {securityMessages}", "description": "Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load." }, + "lighthouse-core/lib/lh-error.js | pageLoadFailedInterstitial": { + "message": "Chrome prevented page load with an interstitial. Make sure you are testing the correct URL and that the server is properly responding to all requests.", + "description": "Error message explaining that Chrome prevented the page from loading and displayed an interstitial screen instead, so the URL cannot be accessed." + }, "lighthouse-core/lib/lh-error.js | pageLoadFailedWithDetails": { "message": "Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})", "description": "Error message explaining that Lighthouse could not load the requested URL and the steps that might be taken to fix the unreliability." diff --git a/lighthouse-core/lib/lh-error.js b/lighthouse-core/lib/lh-error.js index fdbf06d7abfa..cf2c5be0df63 100644 --- a/lighthouse-core/lib/lh-error.js +++ b/lighthouse-core/lib/lh-error.js @@ -23,6 +23,8 @@ const UIStrings = { pageLoadFailedWithDetails: 'Lighthouse was unable to reliably load the page you requested. Make sure you are testing the correct URL and that the server is properly responding to all requests. (Details: {errorDetails})', /** Error message explaining that the security certificate of the page Lighthouse observed was invalid, so the URL cannot be accessed. securityMessages will be replaced with one or more strings from the browser explaining what was insecure about the page load. */ pageLoadFailedInsecure: 'The URL you have provided does not have a valid security certificate. {securityMessages}', + /** Error message explaining that Chrome prevented the page from loading and displayed an interstitial screen instead, so the URL cannot be accessed. */ + pageLoadFailedInterstitial: 'Chrome prevented page load with an interstitial. Make sure you are testing the correct URL and that the server is properly responding to all requests.', /** Error message explaining that Chrome has encountered an error during the Lighthouse run, and that Chrome should be restarted. */ internalChromeError: 'An internal Chrome error occurred. Please restart Chrome and try re-running Lighthouse.', /** Error message explaining that fetching the resources of the webpage has taken longer than the maximum time. */ @@ -180,6 +182,13 @@ const ERRORS = { message: UIStrings.pageLoadFailedInsecure, lhrRuntimeError: true, }, + /* Used when any Chrome interstitial error prevents page load. + */ + CHROME_INTERSTITIAL_ERROR: { + code: 'CHROME_INTERSTITIAL_ERROR', + message: UIStrings.pageLoadFailedInterstitial, + lhrRuntimeError: true, + }, /* Used when the page stopped responding and did not finish loading. */ PAGE_HUNG: { code: 'PAGE_HUNG', diff --git a/lighthouse-core/test/gather/gather-runner-test.js b/lighthouse-core/test/gather/gather-runner-test.js index 3ec3ec8e3a58..7ce077ee5266 100644 --- a/lighthouse-core/test/gather/gather-runner-test.js +++ b/lighthouse-core/test/gather/gather-runner-test.js @@ -820,6 +820,135 @@ describe('GatherRunner', function() { }); }); + describe('#getInterstitialError', () => { + it('passes when the page is loaded', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + expect(GatherRunner.getInterstitialError([mainRecord])).toBeUndefined(); + }); + + it('passes when page fails to load normally', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + mainRecord.failed = true; + mainRecord.localizedFailDescription = 'foobar'; + expect(GatherRunner.getInterstitialError([mainRecord])).toBeUndefined(); + }); + + it('passes when page gets a generic interstitial but somehow also loads everything', () => { + // This case, AFAIK, is impossible, but we'll err on the side of not tanking the run. + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + const interstitialRecord = new NetworkRequest(); + interstitialRecord.url = 'data:text/html;base64,abcdef'; + interstitialRecord.documentURL = 'chrome-error://chromewebdata/'; + const records = [mainRecord, interstitialRecord]; + expect(GatherRunner.getInterstitialError(records)).toBeUndefined(); + }); + + it('fails when page gets a generic interstitial', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + mainRecord.failed = true; + mainRecord.localizedFailDescription = 'ERR_CONNECTION_RESET'; + const interstitialRecord = new NetworkRequest(); + interstitialRecord.url = 'data:text/html;base64,abcdef'; + interstitialRecord.documentURL = 'chrome-error://chromewebdata/'; + const records = [mainRecord, interstitialRecord]; + const error = GatherRunner.getInterstitialError(records); + expect(error.message).toEqual('CHROME_INTERSTITIAL_ERROR'); + expect(error.code).toEqual('CHROME_INTERSTITIAL_ERROR'); + expect(error.friendlyMessage).toBeDisplayString(/^Chrome prevented/); + }); + + it('fails when page gets a security interstitial', () => { + const url = 'http://the-page.com'; + const mainRecord = new NetworkRequest(); + mainRecord.url = url; + mainRecord.failed = true; + mainRecord.localizedFailDescription = 'net::ERR_CERT_COMMON_NAME_INVALID'; + const interstitialRecord = new NetworkRequest(); + interstitialRecord.url = 'data:text/html;base64,abcdef'; + interstitialRecord.documentURL = 'chrome-error://chromewebdata/'; + const records = [mainRecord, interstitialRecord]; + const error = GatherRunner.getInterstitialError(records); + expect(error.message).toEqual('INSECURE_DOCUMENT_REQUEST'); + expect(error.code).toEqual('INSECURE_DOCUMENT_REQUEST'); + expect(error.friendlyMessage).toBeDisplayString(/valid security certificate/); + expect(error.friendlyMessage).toBeDisplayString(/net::ERR_CERT_COMMON_NAME_INVALID/); + }); + }); + + describe('#getPageLoadError', () => { + let navigationError; + + beforeEach(() => { + navigationError = new Error('NAVIGATION_ERROR'); + }); + + it('passes when the page is loaded', () => { + const passContext = {url: 'http://the-page.com', driver: {online: true}}; + const mainRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord]}; + mainRecord.url = passContext.url; + const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); + expect(error).toBeUndefined(); + }); + + it('passes when the page is offline', () => { + const passContext = {url: 'http://the-page.com', driver: {online: false}}; + const mainRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord]}; + mainRecord.url = passContext.url; + mainRecord.failed = true; + + const error = GatherRunner.getPageLoadError(passContext, loadData, undefined); + expect(error).toBeUndefined(); + }); + + it('fails with interstitial error first', () => { + const passContext = {url: 'http://the-page.com', driver: {online: true}}; + const mainRecord = new NetworkRequest(); + const interstitialRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord, interstitialRecord]}; + + mainRecord.url = passContext.url; + mainRecord.failed = true; + interstitialRecord.url = 'data:text/html;base64,abcdef'; + interstitialRecord.documentURL = 'chrome-error://chromewebdata/'; + + const error = GatherRunner.getPageLoadError(passContext, loadData, navigationError); + expect(error.message).toEqual('CHROME_INTERSTITIAL_ERROR'); + }); + + it('fails with network error next', () => { + const passContext = {url: 'http://the-page.com', driver: {online: true}}; + const mainRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord]}; + + mainRecord.url = passContext.url; + mainRecord.failed = true; + + const error = GatherRunner.getPageLoadError(passContext, loadData, navigationError); + expect(error.message).toEqual('FAILED_DOCUMENT_REQUEST'); + }); + + it('fails with nav error last', () => { + const passContext = {url: 'http://the-page.com', driver: {online: true}}; + const mainRecord = new NetworkRequest(); + const loadData = {networkRecords: [mainRecord]}; + + mainRecord.url = passContext.url; + + const error = GatherRunner.getPageLoadError(passContext, loadData, navigationError); + expect(error.message).toEqual('NAVIGATION_ERROR'); + }); + }); + describe('artifact collection', () => { // Make sure our gatherers never execute in parallel it('runs gatherer lifecycle methods strictly in sequence', async () => {