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:
Henrik Skupin
2026-09-10 10:07:47 +00:00
committed by hskupin@mozilla.com
parent d9e39d91f4
commit 7cadebb85c
5 changed files with 224 additions and 18 deletions
+6 -1
View File
@@ -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}).`
+17
View File
@@ -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;
}
}
+107 -17
View File
@@ -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"]