diff --git a/addons/web/static/src/core/errors/error_dialogs.js b/addons/web/static/src/core/errors/error_dialogs.js index ba25914f144..6c16f4b326e 100644 --- a/addons/web/static/src/core/errors/error_dialogs.js +++ b/addons/web/static/src/core/errors/error_dialogs.js @@ -77,7 +77,7 @@ export class RPCErrorDialog extends ErrorDialog { this.inferTitle(); this.traceback = this.props.traceback; if (this.props.data && this.props.data.debug) { - this.traceback = `${this.props.data.debug}`; + this.traceback = `${this.props.data.debug}\nThe above server error caused the following client error:\n${this.traceback}`; } } inferTitle() { diff --git a/addons/web/static/src/core/errors/error_handlers.js b/addons/web/static/src/core/errors/error_handlers.js index 1c32d6f3608..6a4f7054cd3 100644 --- a/addons/web/static/src/core/errors/error_handlers.js +++ b/addons/web/static/src/core/errors/error_handlers.js @@ -20,48 +20,6 @@ const errorHandlerRegistry = registry.category("error_handlers"); const errorDialogRegistry = registry.category("error_dialogs"); const errorNotificationRegistry = registry.category("error_notifications"); -// ----------------------------------------------------------------------------- -// CORS errors -// ----------------------------------------------------------------------------- - -/** - * @param {OdooEnv} env - * @param {UncaughError} error - * @returns {boolean} - */ -function corsErrorHandler(env, error) { - if (error instanceof UncaughtCorsError) { - env.services.dialog.add(NetworkErrorDialog, { - traceback: error.traceback, - message: error.message, - name: error.name, - }); - return true; - } -} -errorHandlerRegistry.add("corsErrorHandler", corsErrorHandler, { sequence: 95 }); - -// ----------------------------------------------------------------------------- -// Client errors -// ----------------------------------------------------------------------------- - -/** - * @param {OdooEnv} env - * @param {UncaughError} error - * @returns {boolean} - */ -function clientErrorHandler(env, error) { - if (error instanceof UncaughtClientError) { - env.services.dialog.add(ClientErrorDialog, { - traceback: error.traceback, - message: error.message, - name: error.name, - }); - return true; - } -} -errorHandlerRegistry.add("clientErrorHandler", clientErrorHandler, { sequence: 96 }); - // ----------------------------------------------------------------------------- // RPC errors // ----------------------------------------------------------------------------- @@ -172,41 +130,27 @@ export function lostConnectionHandler(env, error, originalError) { } errorHandlerRegistry.add("lostConnectionHandler", lostConnectionHandler, { sequence: 98 }); -// ----------------------------------------------------------------------------- -// Empty rejection errors -// ----------------------------------------------------------------------------- - -/** - * @param {OdooEnv} env - * @param {UncaughError} error - * @returns {boolean} - */ -function emptyRejectionErrorHandler(env, error) { - if (!(error instanceof UncaughtPromiseError)) { - return false; - } - env.services.dialog.add(ClientErrorDialog, { - traceback: error.traceback, - message: error.message, - name: error.name, - }); - return true; -} -errorHandlerRegistry.add("emptyRejectionErrorHandler", emptyRejectionErrorHandler, { - sequence: 99, -}); - // ----------------------------------------------------------------------------- // Default handler // ----------------------------------------------------------------------------- +const defaultDialogs = new Map([ + [UncaughtClientError, ClientErrorDialog], + [UncaughtPromiseError, ClientErrorDialog], + [UncaughtCorsError, NetworkErrorDialog], +]); + /** + * Handles the errors based on the very general error categories emitted by the + * error service. Notice how we do not look at the original error at all. + * * @param {OdooEnv} env * @param {UncaughError} error * @returns {boolean} */ function defaultHandler(env, error) { - env.services.dialog.add(ErrorDialog, { + const DialogComponent = defaultDialogs.get(error.constructor) || ErrorDialog; + env.services.dialog.add(DialogComponent, { traceback: error.traceback, message: error.message, name: error.name, diff --git a/addons/web/static/src/core/errors/error_service.js b/addons/web/static/src/core/errors/error_service.js index 563ab3ad8e7..ca83d9438fd 100644 --- a/addons/web/static/src/core/errors/error_service.js +++ b/addons/web/static/src/core/errors/error_service.js @@ -42,7 +42,11 @@ export class UncaughtCorsError extends UncaughtError { export const errorService = { start(env) { - function handleError(error, originalError, retry = true) { + function handleError(uncaughtError, retry = true) { + let originalError = uncaughtError; + while (originalError && "cause" in originalError) { + originalError = originalError.cause; + } const services = env.services; if (!services.dialog || !services.notification || !services.rpc) { // here, the environment is not ready to provide feedback to the user. @@ -50,13 +54,13 @@ export const errorService = { // recover. if (retry) { browser.setTimeout(() => { - handleError(error, originalError, false); + handleError(uncaughtError, false); }, 1000); } return; } for (const handler of registry.category("error_handlers").getAll()) { - if (handler(env, error, originalError)) { + if (handler(env, uncaughtError, originalError)) { break; } } @@ -67,19 +71,19 @@ export const errorService = { ) { // Log the full traceback instead of letting the browser log the incomplete one originalError.errorEvent.preventDefault(); - console.error(error.traceback); + console.error(uncaughtError.traceback); } } browser.addEventListener("error", async (ev) => { - const { colno, error: originalError, filename, lineno, message } = ev; + const { colno, error, filename, lineno, message } = ev; const errorsToIgnore = [ // Ignore some unnecessary "ResizeObserver loop limit exceeded" error in Firefox. "ResizeObserver loop completed with undelivered notifications.", // ignore Chrome video internal error: https://crbug.com/809574 "ResizeObserver loop limit exceeded", ]; - if (!originalError && errorsToIgnore.includes(message)) { + if (!error && errorsToIgnore.includes(message)) { return; } let uncaughtError; @@ -93,25 +97,27 @@ export const errorService = { ); } else { uncaughtError = new UncaughtClientError(); - if (originalError instanceof Error) { - originalError.errorEvent = ev; + if (error instanceof Error) { + error.errorEvent = ev; const annotated = env.debug && env.debug.includes("assets"); - await completeUncaughtError(uncaughtError, originalError, annotated); + await completeUncaughtError(uncaughtError, error, annotated); } } - handleError(uncaughtError, originalError); + uncaughtError.cause = error; + handleError(uncaughtError); }); browser.addEventListener("unhandledrejection", async (ev) => { - const originalError = ev.reason; + const error = ev.reason; const uncaughtError = new UncaughtPromiseError(); uncaughtError.unhandledRejectionEvent = ev; - if (originalError instanceof Error) { - originalError.errorEvent = ev; + if (error instanceof Error) { + error.errorEvent = ev; const annotated = env.debug && env.debug.includes("assets"); - await completeUncaughtError(uncaughtError, originalError, annotated); + await completeUncaughtError(uncaughtError, error, annotated); } - handleError(uncaughtError, originalError); + uncaughtError.cause = error; + handleError(uncaughtError); }); }, }; diff --git a/addons/web/static/src/legacy/legacy_rpc_error_handler.js b/addons/web/static/src/legacy/legacy_rpc_error_handler.js index 63903671536..9321da2fa89 100644 --- a/addons/web/static/src/legacy/legacy_rpc_error_handler.js +++ b/addons/web/static/src/legacy/legacy_rpc_error_handler.js @@ -4,8 +4,6 @@ import { registry } from "@web/core/registry"; import { ConnectionLostError, RPCError } from "../core/network/rpc_service"; import { lostConnectionHandler, rpcErrorHandler } from "@web/core/errors/error_handlers"; -import { OwlError } from "@odoo/owl"; - const errorHandlerRegistry = registry.category("error_handlers"); /** @@ -19,14 +17,11 @@ const errorHandlerRegistry = registry.category("error_handlers"); /** * @param {OdooEnv} env - * @param {Error} error + * @param {UncaughError} error * @param {Error} originalError * @returns {boolean} */ function legacyRPCErrorHandler(env, error, originalError) { - if (originalError instanceof OwlError) { - originalError = originalError.cause; - } if ( originalError && originalError.legacy && diff --git a/addons/web/static/tests/core/errors/error_service_tests.js b/addons/web/static/tests/core/errors/error_service_tests.js index 148621ff8b7..3688e6e0efd 100644 --- a/addons/web/static/tests/core/errors/error_service_tests.js +++ b/addons/web/static/tests/core/errors/error_service_tests.js @@ -8,7 +8,7 @@ import { NetworkErrorDialog, standardErrorDialogProps, } from "@web/core/errors/error_dialogs"; -import { errorService } from "@web/core/errors/error_service"; +import { errorService, UncaughtPromiseError } from "@web/core/errors/error_service"; import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service"; import { notificationService } from "@web/core/notifications/notification_service"; import { registry } from "@web/core/registry"; @@ -21,9 +21,9 @@ import { makeFakeNotificationService, makeFakeRPCService, } from "../../helpers/mock_services"; -import { makeDeferred, nextTick, patchWithCleanup } from "../../helpers/utils"; +import { getFixture, makeDeferred, mount, nextTick, patchWithCleanup } from "../../helpers/utils"; -import { Component, xml } from "@odoo/owl"; +import { Component, xml, onError, OwlError, onWillStart } from "@odoo/owl"; const errorDialogRegistry = registry.category("error_dialogs"); const errorHandlerRegistry = registry.category("error_handlers"); const serviceRegistry = registry.category("services"); @@ -244,6 +244,58 @@ QUnit.test("will let handlers from the registry handle errors first", async (ass assert.verifySteps(["in handler"]); }); +QUnit.test("originalError is the root cause of the error chain", async (assert) => { + errorHandlerRegistry.add("__test_handler__", (env, err, originalError) => { + assert.ok(err instanceof UncaughtPromiseError); // Wrapped by error service + assert.ok(err.cause instanceof OwlError); // Wrapped by owl + assert.strictEqual(err.cause.cause, originalError); // original error + assert.step("in handler"); + }); + const testEnv = await makeTestEnv(); + testEnv.someValue = 14; + const error = new Error(); + error.name = "boom"; + + class ErrHandler extends Component { + setup() { + onError(async (err) => { + await unhandledRejectionCb( + new PromiseRejectionEvent("error", { + reason: err, + promise: null, + cancelable: true, + }) + ); + prom.resolve(); + }); + } + } + ErrHandler.template = xml``; + class ThrowInSetup extends Component { + setup() { + throw error; + } + } + ThrowInSetup.template = xml``; + let prom = makeDeferred(); + mount(ErrHandler, getFixture(), { props: { comp: ThrowInSetup } }); + await prom; + assert.verifySteps(["in handler"]); + + class ThrowInWillStart extends Component { + setup() { + onWillStart(() => { + throw error; + }); + } + } + ThrowInWillStart.template = xml``; + prom = makeDeferred(); + mount(ErrHandler, getFixture(), { props: { comp: ThrowInWillStart } }); + await prom; + assert.verifySteps(["in handler"]); +}); + QUnit.test("handle uncaught promise errors", async (assert) => { class TestError extends Error {} const error = new TestError();