[IMP] web: use the error class to handle errors

Before this commit, error handlers checked the error name to know if
they should be executed.

Now, they check the instance type of the error to be more accurate on which
error an handler deals with.

This commit also changes `emptyRejectionErrorHandler` sequence number
because it had more priority than `rpcErrorHandler` and `lostConnectionHandler`.
This commit is contained in:
Michael Mattiello (mcm)
2021-06-18 21:31:29 +02:00
committed by Aaron Bohy
parent 45aaaf40dc
commit b59024f6ba
4 changed files with 122 additions and 57 deletions
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { browser } from "../browser/browser";
import { ConnectionLostError, RPCError } from "../network/rpc_service";
import { registry } from "../registry";
import {
ClientErrorDialog,
@@ -8,6 +9,7 @@ import {
NetworkErrorDialog,
RPCErrorDialog,
} from "./error_dialogs";
import { UncaughtClientError, UncaughtCorsError, UncaughtPromiseError } from "./error_service";
/**
* @typedef {import("../../env").OdooEnv} OdooEnv
@@ -28,7 +30,7 @@ const errorDialogRegistry = registry.category("error_dialogs");
*/
function corsErrorHandler(env) {
return (error) => {
if (error.name === "UNKNOWN_CORS_ERROR") {
if (error instanceof UncaughtCorsError) {
env.services.dialog.open(NetworkErrorDialog, {
traceback: error.traceback || error.stack,
message: error.message,
@@ -50,7 +52,7 @@ errorHandlerRegistry.add("corsErrorHandler", corsErrorHandler, { sequence: 95 })
*/
function clientErrorHandler(env) {
return (error) => {
if (error.name === "UNCAUGHT_CLIENT_ERROR") {
if (error instanceof UncaughtClientError) {
env.services.dialog.open(ClientErrorDialog, {
traceback: error.traceback || error.stack,
message: error.message,
@@ -62,29 +64,6 @@ function clientErrorHandler(env) {
}
errorHandlerRegistry.add("clientErrorHandler", clientErrorHandler, { sequence: 96 });
// -----------------------------------------------------------------------------
// Empty rejection errors
// -----------------------------------------------------------------------------
/**
* @param {OdooEnv} env
* @returns {ErrorHandler}
*/
function emptyRejectionErrorHandler(env) {
return (error) => {
if (error.name === "UNCAUGHT_EMPTY_REJECTION_ERROR") {
env.services.dialog.open(ClientErrorDialog, {
message: error.message,
name: error.name,
});
return true;
}
};
}
errorHandlerRegistry.add("emptyRejectionErrorHandler", emptyRejectionErrorHandler, {
sequence: 97,
});
// -----------------------------------------------------------------------------
// RPC errors
// -----------------------------------------------------------------------------
@@ -95,8 +74,11 @@ errorHandlerRegistry.add("emptyRejectionErrorHandler", emptyRejectionErrorHandle
*/
function rpcErrorHandler(env) {
return (uncaughtError) => {
if (!(uncaughtError instanceof UncaughtPromiseError)) {
return;
}
const error = uncaughtError.originalError;
if (error && error.name === "RPC_ERROR") {
if (error instanceof RPCError) {
// When an error comes from the server, it can have an exeption name.
// (or any string truly). It is used as key in the error dialog from
// server registry to know which dialog component to use.
@@ -106,13 +88,13 @@ function rpcErrorHandler(env) {
// error is here a RPCError
uncaughtError.unhandledRejectionEvent.preventDefault();
const exceptionName = error.exceptionName;
let ErrorComponent = error.Component;
if (!ErrorComponent && exceptionName && errorDialogRegistry.contains(exceptionName)) {
let ErrorComponent = null;
if (exceptionName && errorDialogRegistry.contains(exceptionName)) {
ErrorComponent = errorDialogRegistry.get(exceptionName);
}
env.services.dialog.open(ErrorComponent || RPCErrorDialog, {
traceback: error.traceback || error.stack,
traceback: error.stack,
message: error.message,
name: error.name,
exceptionName: error.exceptionName,
@@ -125,7 +107,7 @@ function rpcErrorHandler(env) {
}
};
}
errorHandlerRegistry.add("rpcErrorHandler", rpcErrorHandler, { sequence: 98 });
errorHandlerRegistry.add("rpcErrorHandler", rpcErrorHandler, { sequence: 97 });
// -----------------------------------------------------------------------------
// Lost connection errors
@@ -139,7 +121,7 @@ function lostConnectionHandler(env) {
let connectionLostNotifId;
return (uncaughtError) => {
const error = uncaughtError.originalError;
if (error && error.name === "CONNECTION_LOST_ERROR") {
if (error instanceof ConnectionLostError) {
if (connectionLostNotifId) {
// notification already displayed (can occur if there were several
// concurrent rpcs when the connection was lost)
@@ -163,7 +145,7 @@ function lostConnectionHandler(env) {
}
);
})
.catch((e) => {
.catch(() => {
// exponential backoff, with some jitter
delay = delay * 1.5 + 500 * Math.random();
browser.setTimeout(checkConnection, delay);
@@ -173,7 +155,32 @@ function lostConnectionHandler(env) {
}
};
}
errorHandlerRegistry.add("lostConnectionHandler", lostConnectionHandler, { sequence: 99 });
errorHandlerRegistry.add("lostConnectionHandler", lostConnectionHandler, { sequence: 98 });
// -----------------------------------------------------------------------------
// Empty rejection errors
// -----------------------------------------------------------------------------
/**
* @param {OdooEnv} env
* @returns {ErrorHandler}
*/
function emptyRejectionErrorHandler(env) {
return (uncaughtError) => {
if (uncaughtError instanceof UncaughtPromiseError) {
const error = uncaughtError.originalError;
env.services.dialog.open(ClientErrorDialog, {
traceback: error.traceback || error.stack,
message: error.message,
name: error.name,
});
return true;
}
};
}
errorHandlerRegistry.add("emptyRejectionErrorHandler", emptyRejectionErrorHandler, {
sequence: 99,
});
// -----------------------------------------------------------------------------
// Default handler
@@ -185,8 +192,7 @@ errorHandlerRegistry.add("lostConnectionHandler", lostConnectionHandler, { seque
*/
function defaultHandler(env) {
return (error) => {
const DialogComponent = error.Component || ErrorDialog;
env.services.dialog.open(DialogComponent, {
env.services.dialog.open(ErrorDialog, {
traceback: error.traceback || error.stack,
message: error.message,
name: error.name,
@@ -22,20 +22,20 @@ export class UncaughtError extends Error {
}
}
class UncaughtClientError extends UncaughtError {
export class UncaughtClientError extends UncaughtError {
constructor(message = _lt("Uncaught Javascript Error")) {
super(message, "UncaughtClientError");
}
}
class UncaughtPromiseError extends UncaughtError {
export class UncaughtPromiseError extends UncaughtError {
constructor(message = _lt("Uncaught Promise")) {
super(message, "UncaughtPromiseError");
this.unhandledRejectionEvent = null;
}
}
class UncaughtCorsError extends UncaughtError {
export class UncaughtCorsError extends UncaughtError {
constructor(message = _lt("Uncaught CORS Error")) {
super(message, "UncaughtCorsError");
}
@@ -11,22 +11,16 @@ export class RPCError extends Error {
super(...arguments);
this.name = "RPC_ERROR";
this.type = "server";
this.code = null;
this.data = null;
this.exceptionName = null;
this.subType = null;
}
}
export class ConnectionLostError extends Error {
constructor() {
super(...arguments);
this.name = "CONNECTION_LOST_ERROR";
}
}
export class ConnectionLostError extends Error {}
export class ConnectionAbortedError extends Error {
constructor() {
super(...arguments);
this.name = "CONNECTION_ABORTED_ERROR";
}
}
export class ConnectionAbortedError extends Error {}
// -----------------------------------------------------------------------------
// Main RPC method
@@ -2,7 +2,11 @@
import { browser } from "@web/core/browser/browser";
import { dialogService } from "@web/core/dialog/dialog_service";
import { RPCErrorDialog } from "@web/core/errors/error_dialogs";
import {
ClientErrorDialog,
RPCErrorDialog,
NetworkErrorDialog,
} from "@web/core/errors/error_dialogs";
import { errorService } from "@web/core/errors/error_service";
import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service";
import { notificationService } from "@web/core/notifications/notification_service";
@@ -30,7 +34,7 @@ function makeFakeDialogService(open) {
};
}
let errorCb; // unused ?
let errorCb;
let unhandledRejectionCb;
QUnit.module("Error Service", {
@@ -73,7 +77,7 @@ QUnit.test("handle RPC_ERROR of type='server' and no associated dialog class", a
},
subType: "strange_error",
message: "Some strange error occured",
exceptionName: undefined,
exceptionName: null,
traceback: error.stack,
});
}
@@ -93,17 +97,17 @@ QUnit.test(
const error = new RPCError();
error.code = 701;
error.message = "Some strange error occured";
error.Component = CustomDialog;
error.exceptionName = "strange_error";
function open(dialogClass, props) {
assert.strictEqual(dialogClass, CustomDialog);
assert.deepEqual(props, {
name: "RPC_ERROR",
type: "server",
code: 701,
data: undefined,
subType: undefined,
data: null,
subType: null,
message: "Some strange error occured",
exceptionName: undefined,
exceptionName: "strange_error",
traceback: error.stack,
});
}
@@ -172,3 +176,64 @@ QUnit.test("will let handlers from the registry handle errors first", async (ass
unhandledRejectionCb(errorEvent);
assert.verifySteps(["in handler"]);
});
QUnit.test("handle uncaught promise errors", async (assert) => {
class TestError extends Error {}
const error = new TestError();
error.message = "This is an error test";
error.name = "TestError";
function open(dialogClass, props) {
assert.strictEqual(dialogClass, ClientErrorDialog);
assert.deepEqual(props, {
name: "TestError",
message: "This is an error test",
traceback: error.stack,
});
}
serviceRegistry.add("dialog", makeFakeDialogService(open), { force: true });
await makeTestEnv();
const errorEvent = new PromiseRejectionEvent("error", { reason: error, promise: null });
unhandledRejectionCb(errorEvent);
});
QUnit.test("handle uncaught client errors", async (assert) => {
class TestError extends Error {}
const error = new TestError();
error.message = "This is an error test";
error.name = "TestError";
function open(dialogClass, props) {
assert.strictEqual(dialogClass, ClientErrorDialog);
assert.strictEqual(props.message, "Uncaught Javascript Error");
}
serviceRegistry.add("dialog", makeFakeDialogService(open), { force: true });
await makeTestEnv();
const errorEvent = new ErrorEvent("error", {
error,
colno: 1,
lineno: 1,
filename: "test",
});
errorCb(errorEvent);
});
QUnit.test("handle uncaught CORS errors", async (assert) => {
class TestError extends Error {}
const error = new TestError();
error.message = "This is a cors error";
error.name = "CORS error";
function open(dialogClass, props) {
assert.strictEqual(dialogClass, NetworkErrorDialog);
assert.strictEqual(props.message, "Uncaught CORS Error");
}
serviceRegistry.add("dialog", makeFakeDialogService(open), { force: true });
await makeTestEnv();
// CORS error event has no colno, no lineno and no filename
const errorEvent = new ErrorEvent("error", { error });
errorCb(errorEvent);
});