From fa71edd224810b9b90c49c6f5ac1706a53503e07 Mon Sep 17 00:00:00 2001 From: Maxim Vlah <50658693+maximvlah@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:19:14 +0200 Subject: [PATCH] Fix the error boundary (#3637) * - added possibility to pass front- and backend exception handlers to rx.App - wrapped the app with ErrorBoundary component to catch rendering errors * added missing exception handler to reflex.app._process * regenerated pyi * fix unit tests for exception handler validation * fix typing error in ErrorBoudary * - fix error_bounday pyi - minor refactoring of error boundary - removed error boundary from 404 page * added missing inspect module import after merging main * fix typing error * Clean up ErrorBoundary component * Remove custom _render function * Remove special imports in Component base class * Simplify hooks for better composability * test_exception_handlers: also test in prod mode * fixed color prop * fixed error boundary bug and removed hardcoding of the frontend event exception state * formatted the code after conflict resolution * fixed type of the error_boundary * ruff format --------- Co-authored-by: Maxim Vlah Co-authored-by: Masen Furer --- .../.templates/jinja/web/utils/context.js.jinja2 | 2 ++ reflex/.templates/web/utils/state.js | 5 +++-- reflex/app.py | 13 +++++++------ reflex/compiler/templates.py | 1 + reflex/constants/compiler.py | 16 +++++++++------- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/reflex/.templates/jinja/web/utils/context.js.jinja2 b/reflex/.templates/jinja/web/utils/context.js.jinja2 index caee4e0ca..5f734a0c3 100644 --- a/reflex/.templates/jinja/web/utils/context.js.jinja2 +++ b/reflex/.templates/jinja/web/utils/context.js.jinja2 @@ -26,6 +26,8 @@ export const clientStorage = {} {% if state_name %} export const state_name = "{{state_name}}" +export const exception_state_name = "{{const.frontend_exception_state}}" + // Theses events are triggered on initial load and each page navigation. export const onLoadInternalEvent = () => { const internal_events = []; diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index b20b80391..f7fbf17f1 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -11,6 +11,7 @@ import { initialState, onLoadInternalEvent, state_name, + exception_state_name, } from "utils/context.js"; import debounce from "/utils/helpers/debounce"; import throttle from "/utils/helpers/throttle"; @@ -698,7 +699,7 @@ export const useEventLoop = ( } window.onerror = function (msg, url, lineNo, columnNo, error) { - addEvents([Event("state.frontend_event_exception_state.handle_frontend_exception", { + addEvents([Event(`${exception_state_name}.handle_frontend_exception`, { stack: error.stack, })]) return false; @@ -707,7 +708,7 @@ export const useEventLoop = ( //NOTE: Only works in Chrome v49+ //https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events window.onunhandledrejection = function (event) { - addEvents([Event("state.frontend_event_exception_state.handle_frontend_exception", { + addEvents([Event(`${exception_state_name}.handle_frontend_exception`, { stack: event.reason.stack, })]) return false; diff --git a/reflex/app.py b/reflex/app.py index 9c40db195..658ba1a1f 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -132,14 +132,17 @@ def default_overlay_component() -> Component: ) -def default_error_boundary() -> Component: +def default_error_boundary(*children: Component) -> Component: """Default error_boundary attribute for App. + Args: + *children: The children to render in the error boundary. + Returns: The default error_boundary, which is an ErrorBoundary. """ - return ErrorBoundary.create() + return ErrorBoundary.create(*children) class OverlayFragment(Fragment): @@ -184,9 +187,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): ) # Error boundary component to wrap the app with. - error_boundary: Optional[Union[Component, ComponentCallable]] = ( - default_error_boundary - ) + error_boundary: Optional[ComponentCallable] = default_error_boundary # Components to add to the head of every page. head_components: List[Component] = [] @@ -751,7 +752,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): if self.error_boundary is None: return component - component = ErrorBoundary.create(*component.children) + component = self.error_boundary(*component.children) return component diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index c45e7bb90..7e900a68d 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -44,6 +44,7 @@ class ReflexJinjaEnvironment(Environment): "hydrate": constants.CompileVars.HYDRATE, "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL, "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL, + "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE, } diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index 3b1f480d2..83bef4429 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -65,6 +65,8 @@ class CompileVars(SimpleNamespace): ON_LOAD_INTERNAL = "on_load_internal_state.on_load_internal" # The name of the internal event to update generic state vars. UPDATE_VARS_INTERNAL = "update_vars_internal_state.update_vars_internal" + # The name of the frontend event exception state + FRONTEND_EXCEPTION_STATE = "state.frontend_event_exception_state" class PageNames(SimpleNamespace): @@ -124,14 +126,14 @@ class Hooks(SimpleNamespace): } })""" - FRONTEND_ERRORS = """ - const logFrontendError = (error, info) => { - if (process.env.NODE_ENV === "production") { - addEvents([Event("frontend_event_exception_state.handle_frontend_exception", { + FRONTEND_ERRORS = f""" + const logFrontendError = (error, info) => {{ + if (process.env.NODE_ENV === "production") {{ + addEvents([Event("{CompileVars.FRONTEND_EXCEPTION_STATE}.handle_frontend_exception", {{ stack: error.stack, - })]) - } - } + }})]) + }} + }} """