Browse Source

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 <m.vlah@senbax.de>
Co-authored-by: Masen Furer <m_github@0x26.net>
Maxim Vlah 10 months ago
parent
commit
fa71edd224

+ 2 - 0
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 = [];

+ 3 - 2
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;

+ 7 - 6
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
 

+ 1 - 0
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,
         }
 
 

+ 9 - 7
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,
-            })])
-        }
-    }
+            }})])
+        }}
+    }}
     """