Bug 2070300 - [remote] Throw "NavigationError" instead of a plain "Error" in ProgressListener. r=Sasha
Differential Revision: https://phabricator.services.mozilla.com/D324385
This commit is contained in:
committed by
hskupin@mozilla.com
parent
d9e39d91f4
commit
7cadebb85c
@@ -145,6 +145,7 @@ export class ProgressListener {
|
||||
|
||||
#deferredNavigation;
|
||||
#errorName;
|
||||
#errorStatus;
|
||||
#navigationId;
|
||||
#navigationListener;
|
||||
#seenNavigationCommitted;
|
||||
@@ -209,6 +210,7 @@ export class ProgressListener {
|
||||
|
||||
this.#deferredNavigation = null;
|
||||
this.#errorName = null;
|
||||
this.#errorStatus = null;
|
||||
this.#resolveWhenCommittedError = null;
|
||||
this.#seenNavigationCommitted = false;
|
||||
this.#seenStartFlag = false;
|
||||
@@ -339,6 +341,7 @@ export class ProgressListener {
|
||||
this.#trace(`Error=${errorName}, wait for redirect to error page`);
|
||||
this.#seenNavigationCommitted = false;
|
||||
this.#errorName = errorName;
|
||||
this.#errorStatus = status;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -446,7 +449,9 @@ export class ProgressListener {
|
||||
);
|
||||
if (this.#seenNavigationCommitted || !this.#navigationListener) {
|
||||
// If the navigation-committed event was already received, resolve immediately
|
||||
this.stop({ error: new Error(errorName) });
|
||||
this.stop({
|
||||
error: new lazy.NavigationError(errorName, this.#errorStatus),
|
||||
});
|
||||
} else {
|
||||
this.#trace(
|
||||
`Waiting for the "navigation-committed" event for the error page navigation (error: ${errorName}).`
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
* 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/. */
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "nssErrorsService", () =>
|
||||
Cc["@mozilla.org/nss_errors_service;1"].getService(Ci.nsINSSErrorsService)
|
||||
);
|
||||
|
||||
/**
|
||||
* Base class for all remote protocol errors.
|
||||
*/
|
||||
@@ -36,4 +42,15 @@ export class NavigationError extends Error {
|
||||
get isBindingAborted() {
|
||||
return this.#status == Cr.NS_BINDING_ABORTED;
|
||||
}
|
||||
|
||||
get isCertError() {
|
||||
try {
|
||||
return (
|
||||
lazy.nssErrorsService.getErrorClass(this.#status) ===
|
||||
Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT
|
||||
);
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,18 @@ const { isInitialDocument, isUncommittedInitialDocument } =
|
||||
|
||||
const LOAD_FLAG_ERROR_PAGE = 0x10000;
|
||||
|
||||
// Force the initialization of NSS, without which PR_ErrorToName() cannot
|
||||
// resolve NSS error codes and ChromeUtils.getXPCOMErrorName() asserts.
|
||||
Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
|
||||
|
||||
const SEC_ERROR_EXPIRED_CERTIFICATE = Cc["@mozilla.org/nss_errors_service;1"]
|
||||
.getService(Ci.nsINSSErrorsService)
|
||||
.getXPCOMFromNSSError(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE + 11);
|
||||
|
||||
const CURRENT_URI = Services.io.newURI("http://foo.bar/");
|
||||
const ERROR_PAGE_URI = Services.io.newURI(
|
||||
"about:neterror?e=customErrorMessage"
|
||||
);
|
||||
const INITIAL_URI = Services.io.newURI("about:blank");
|
||||
const TARGET_URI = Services.io.newURI("http://foo.cheese/");
|
||||
const TARGET_URI_ERROR_PAGE = Services.io.newURI("doesnotexist://");
|
||||
@@ -39,6 +50,16 @@ function wait(time) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
async function getRejectionReason(promise) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class MockRequest {
|
||||
constructor(uri) {
|
||||
this.originalURI = uri;
|
||||
@@ -172,7 +193,7 @@ class MockTopContext {
|
||||
updateURI(uri, isError = false) {
|
||||
this.currentURI = uri;
|
||||
if (isError) {
|
||||
this.currentWindowGlobal.documentURI = "about:neterror?e=errorMessage";
|
||||
this.currentWindowGlobal.documentURI = ERROR_PAGE_URI;
|
||||
} else {
|
||||
this.currentWindowGlobal.documentURI = uri;
|
||||
}
|
||||
@@ -808,25 +829,48 @@ add_task(async function test_ProgressListener_ignoreCacheError() {
|
||||
});
|
||||
|
||||
add_task(async function test_ProgressListener_navigationRejectedOnErrorPage() {
|
||||
const browsingContext = new MockTopContext();
|
||||
const webProgress = browsingContext.webProgress;
|
||||
const testCases = [
|
||||
{
|
||||
description: "error page",
|
||||
flag: Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE,
|
||||
expectedErrorName: "customErrorMessage",
|
||||
},
|
||||
{
|
||||
// For a same-document navigation the document URI is not replaced by an
|
||||
// error page URI, and no error name can be extracted from it.
|
||||
description: "same-document error page",
|
||||
flag:
|
||||
Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT |
|
||||
Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE,
|
||||
expectedErrorName: "Address rejected",
|
||||
},
|
||||
];
|
||||
|
||||
const progressListener = new ProgressListener(webProgress, {
|
||||
waitForExplicitStart: false,
|
||||
});
|
||||
const navigated = progressListener.start();
|
||||
for (const { description, flag, expectedErrorName } of testCases) {
|
||||
info(`Checking location change for ${description}`);
|
||||
|
||||
await webProgress.sendStartState();
|
||||
await webProgress.sendLocationChange({
|
||||
flag:
|
||||
Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT |
|
||||
Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE,
|
||||
});
|
||||
const browsingContext = new MockTopContext();
|
||||
const webProgress = browsingContext.webProgress;
|
||||
|
||||
ok(
|
||||
await hasPromiseRejected(navigated),
|
||||
"Listener has rejected in location change for error page"
|
||||
);
|
||||
const progressListener = new ProgressListener(webProgress, {
|
||||
waitForExplicitStart: false,
|
||||
});
|
||||
const navigated = progressListener.start();
|
||||
|
||||
await webProgress.sendStartState();
|
||||
await webProgress.sendLocationChange({ flag });
|
||||
|
||||
ok(
|
||||
await hasPromiseRejected(navigated),
|
||||
"Listener has rejected in location change for error page"
|
||||
);
|
||||
|
||||
const error = await getRejectionReason(navigated);
|
||||
ok(error.isNavigationError, "Rejected with a NavigationError");
|
||||
equal(error.message, expectedErrorName, "Expected error name is set");
|
||||
ok(!error.isBindingAborted, "Error is not reported as aborted");
|
||||
ok(!error.isCertError, "Error is not reported as a certificate error");
|
||||
}
|
||||
});
|
||||
|
||||
add_task(
|
||||
@@ -849,6 +893,44 @@ add_task(
|
||||
await hasPromiseRejected(navigated),
|
||||
"Listener has rejected in stop state for erroneous navigation"
|
||||
);
|
||||
|
||||
const error = await getRejectionReason(navigated);
|
||||
ok(error.isNavigationError, "Rejected with a NavigationError");
|
||||
equal(
|
||||
error.message,
|
||||
"NS_ERROR_MALWARE_URI",
|
||||
"Error name from the stop state was kept"
|
||||
);
|
||||
ok(!error.isBindingAborted, "Error is not reported as aborted");
|
||||
ok(!error.isCertError, "Error is not reported as a certificate error");
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_ProgressListener_navigationRejectedOnStopStateCertErrorPage() {
|
||||
const browsingContext = new MockTopContext();
|
||||
const webProgress = browsingContext.webProgress;
|
||||
|
||||
const progressListener = new ProgressListener(webProgress, {
|
||||
waitForExplicitStart: false,
|
||||
});
|
||||
const navigated = progressListener.start();
|
||||
|
||||
await webProgress.sendStartState();
|
||||
await webProgress.sendStopState({
|
||||
flag: SEC_ERROR_EXPIRED_CERTIFICATE,
|
||||
loadType: LOAD_FLAG_ERROR_PAGE,
|
||||
});
|
||||
|
||||
ok(
|
||||
await hasPromiseRejected(navigated),
|
||||
"Listener has rejected in stop state for erroneous navigation"
|
||||
);
|
||||
|
||||
const error = await getRejectionReason(navigated);
|
||||
ok(error.isNavigationError, "Rejected with a NavigationError");
|
||||
ok(error.isCertError, "Error is reported as a certificate error");
|
||||
ok(!error.isBindingAborted, "Error is not reported as aborted");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -870,6 +952,14 @@ add_task(
|
||||
await hasPromiseRejected(navigated),
|
||||
"Listener has rejected in stop state for erroneous navigation"
|
||||
);
|
||||
|
||||
const error = await getRejectionReason(navigated);
|
||||
ok(error.isNavigationError, "Rejected with a NavigationError");
|
||||
equal(
|
||||
error.isBindingAborted,
|
||||
flag === Cr.NS_BINDING_ABORTED,
|
||||
"Expected isBindingAborted value"
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/* 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/. */
|
||||
|
||||
const { NavigationError } = ChromeUtils.importESModule(
|
||||
"chrome://remote/content/shared/RemoteError.sys.mjs"
|
||||
);
|
||||
|
||||
const nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
|
||||
Ci.nsINSSErrorsService
|
||||
);
|
||||
|
||||
const SEC_ERROR_EXPIRED_CERTIFICATE = nssErrorsService.getXPCOMFromNSSError(
|
||||
Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE + 11
|
||||
);
|
||||
|
||||
const SSL_ERROR_NO_CYPHER_OVERLAP = nssErrorsService.getXPCOMFromNSSError(
|
||||
Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE + 2
|
||||
);
|
||||
|
||||
add_task(async function test_fixtures() {
|
||||
equal(
|
||||
nssErrorsService.getErrorClass(SEC_ERROR_EXPIRED_CERTIFICATE),
|
||||
Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT,
|
||||
"SEC_ERROR_EXPIRED_CERTIFICATE is a certificate error"
|
||||
);
|
||||
equal(
|
||||
nssErrorsService.getErrorClass(SSL_ERROR_NO_CYPHER_OVERLAP),
|
||||
Ci.nsINSSErrorsService.ERROR_CLASS_SSL_PROTOCOL,
|
||||
"SSL_ERROR_NO_CYPHER_OVERLAP is a SSL protocol error"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_NavigationError_basics() {
|
||||
const error = new NavigationError(
|
||||
"NS_ERROR_UNKNOWN_HOST",
|
||||
Cr.NS_ERROR_UNKNOWN_HOST
|
||||
);
|
||||
|
||||
// Cannot use `instanceof` because the module is loaded in a different realm.
|
||||
ok(Error.isError(error), "NavigationError is an Error");
|
||||
ok(error.isNavigationError, "Is flagged as a navigation error");
|
||||
equal(error.message, "NS_ERROR_UNKNOWN_HOST", "Expected message is set");
|
||||
});
|
||||
|
||||
add_task(async function test_NavigationError_isBindingAborted() {
|
||||
const aborted = new NavigationError(
|
||||
"NS_BINDING_ABORTED",
|
||||
Cr.NS_BINDING_ABORTED
|
||||
);
|
||||
ok(aborted.isBindingAborted, "NS_BINDING_ABORTED is detected");
|
||||
|
||||
for (const status of [
|
||||
Cr.NS_OK,
|
||||
Cr.NS_ERROR_ABORT,
|
||||
Cr.NS_ERROR_UNKNOWN_HOST,
|
||||
SEC_ERROR_EXPIRED_CERTIFICATE,
|
||||
]) {
|
||||
const error = new NavigationError("errorName", status);
|
||||
ok(
|
||||
!error.isBindingAborted,
|
||||
`Status 0x${status.toString(16)} is not detected as aborted`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_NavigationError_isCertError() {
|
||||
const certError = new NavigationError(
|
||||
"SEC_ERROR_EXPIRED_CERTIFICATE",
|
||||
SEC_ERROR_EXPIRED_CERTIFICATE
|
||||
);
|
||||
ok(certError.isCertError, "Certificate error is detected");
|
||||
|
||||
const sslError = new NavigationError(
|
||||
"SSL_ERROR_NO_CYPHER_OVERLAP",
|
||||
SSL_ERROR_NO_CYPHER_OVERLAP
|
||||
);
|
||||
ok(!sslError.isCertError, "SSL protocol error is not a certificate error");
|
||||
});
|
||||
|
||||
add_task(async function test_NavigationError_isCertError_nonNSSStatus() {
|
||||
for (const status of [
|
||||
undefined,
|
||||
null,
|
||||
Cr.NS_OK,
|
||||
Cr.NS_BINDING_ABORTED,
|
||||
Cr.NS_ERROR_UNKNOWN_HOST,
|
||||
]) {
|
||||
const error = new NavigationError("errorName", status);
|
||||
ok(!error.isCertError, `Status ${status} is not a certificate error`);
|
||||
}
|
||||
});
|
||||
@@ -21,6 +21,8 @@ head = "head.js"
|
||||
|
||||
["test_RecommendedPreferences.js"]
|
||||
|
||||
["test_RemoteError.js"]
|
||||
|
||||
["test_Stack.js"]
|
||||
|
||||
["test_Sync.js"]
|
||||
|
||||
Reference in New Issue
Block a user