From e1bbd622e56d35e59dd915e560f012f17c6b93f8 Mon Sep 17 00:00:00 2001 From: Tobias Van Damme <76486520+aGitForEveryone@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:54:41 +0200 Subject: [PATCH 1/5] Added check to see if componentPath is defined If a component defined in running does not exist, the componentPath in updateComponent will be undefined. Added a check that skips the update if that is the case. Otherwise the dashboard will crash as it will try to perform operations on the undefined object that it cannot perform. --- dash/dash-renderer/src/actions/callbacks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 6eac640d12..807624bb4a 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -334,6 +334,11 @@ function updateComponent(component_id: any, props: any) { return function (dispatch: any, getState: any) { const paths = getState().paths; const componentPath = getPath(paths, component_id); + if (typeof componentPath === 'undefined') { + // Can't find the component that was defined in the running keyword, + // Let's skip the component to prevent the dashboard from crashing. + return; + } dispatch( updateProps({ props, From 0d716113a54834678835b8b78afba6a70b9f7366 Mon Sep 17 00:00:00 2001 From: Tobias Van Damme <76486520+aGitForEveryone@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:56:07 +0200 Subject: [PATCH 2/5] Added test to verify that dashboard does not crash when calling a non-existent component in the running keyword. --- .../callbacks/test_basic_callback.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c8f4158b78..2a344fb3ea 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -823,3 +823,38 @@ def on_click(_): dash_duo.wait_for_text_to_equal("#output", "done") dash_duo.wait_for_text_to_equal("#running", "off") + + +def test_cbsc020_callback_running_non_existing_component(dash_duo): + lock = Lock() + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Button("start", id="start"), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("start", "n_clicks"), + running=[ + [ + Output("non_existent_component", "children"), + html.B("on", id="content"), + "off", + ] + ], + prevent_initial_call=True, + ) + def on_click(_): + with lock: + pass + return "done" + + dash_duo.start_server(app) + with lock: + dash_duo.find_element("#start").click() + + dash_duo.wait_for_text_to_equal("#output", "done") From caa44613fc918d1bc63997c5c615b475d896f0bb Mon Sep 17 00:00:00 2001 From: Tobias Van Damme <76486520+aGitForEveryone@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:06:44 +0200 Subject: [PATCH 3/5] Added changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90229a8977..533a5bd932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#2881](https://github.com/plotly/dash/pull/2881) Add outputs_list to window.dash_clientside.callback_context. Fixes [#2877](https://github.com/plotly/dash/issues/2877). +## Fixed + +- [#2898](https://github.com/plotly/dash/pull/2898) Fix error thrown when using non-existent components in callback running keyword. Fixes [#2897](https://github.com/plotly/dash/issues/2897). + ## [2.17.1] - 2024-06-12 ## Fixed From 7c172b436737a1da8d6dfcce5547cdef02a8f465 Mon Sep 17 00:00:00 2001 From: Tobias Van Damme <76486520+aGitForEveryone@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:27:43 +0200 Subject: [PATCH 4/5] Only do silent pass if surpress_callback_exceptions=True, otherwise throw error Non-existent running components will now only silently passed if surpress_callback_exceptions=True. Otherwise, an error will be thrown. Note: even if an error is thrown, the callback execution is not stopped. Only the side-update of the running component is ignored. --- dash/dash-renderer/src/actions/callbacks.ts | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 807624bb4a..84907b7a17 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -36,7 +36,7 @@ import { } from '../types/callbacks'; import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies'; import {urlBase} from './utils'; -import {getCSRFHeader} from '.'; +import {getCSRFHeader, dispatchError} from '.'; import {createAction, Action} from 'redux-actions'; import {addHttpHeaders} from '../actions'; import {notifyObservers, updateProps} from './index'; @@ -330,13 +330,27 @@ async function handleClientside( return result; } -function updateComponent(component_id: any, props: any) { +function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { return function (dispatch: any, getState: any) { - const paths = getState().paths; + const {paths, config} = getState(); const componentPath = getPath(paths, component_id); - if (typeof componentPath === 'undefined') { - // Can't find the component that was defined in the running keyword, - // Let's skip the component to prevent the dashboard from crashing. + if (!componentPath) { + if (!config.suppress_callback_exceptions) { + dispatchError(dispatch)( + 'ID running component not found in layout', + [ + 'Component defined in running keyword not found in layout.', + `Component id: "${stringifyId(component_id)}"`, + 'This ID was used in the callback(s) for Output(s):', + `${cb.output}`, + 'You can suppress this exception by setting', + '`suppress_callback_exceptions=True`.' + ] + ); + } + // We need to stop further processing because functions further on + // can't operate on an 'undefined' object, and they will throw an + // error. return; } dispatch( @@ -386,7 +400,7 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) { return acc; }, [] as any[]) .forEach(([id, idProps]) => { - dispatch(updateComponent(id, idProps)); + dispatch(updateComponent(id, idProps, cb)); }); }; } From 014bec7ee05a7e6a7cc6fec42bd90b84d18107cf Mon Sep 17 00:00:00 2001 From: Tobias Van Damme <76486520+aGitForEveryone@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:29:57 +0200 Subject: [PATCH 5/5] Added test to check for error messages when using non-existent running components and not suppressing callback exceptions --- .../callbacks/test_basic_callback.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 2a344fb3ea..d22e1c3e00 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -827,7 +827,7 @@ def on_click(_): def test_cbsc020_callback_running_non_existing_component(dash_duo): lock = Lock() - app = Dash(__name__) + app = Dash(__name__, suppress_callback_exceptions=True) app.layout = html.Div( [ @@ -858,3 +858,60 @@ def on_click(_): dash_duo.find_element("#start").click() dash_duo.wait_for_text_to_equal("#output", "done") + + +def test_cbsc021_callback_running_non_existing_component(dash_duo): + lock = Lock() + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Button("start", id="start"), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("start", "n_clicks"), + running=[ + [ + Output("non_existent_component", "children"), + html.B("on", id="content"), + "off", + ] + ], + prevent_initial_call=True, + ) + def on_click(_): + with lock: + pass + return "done" + + dash_duo.start_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + with lock: + dash_duo.find_element("#start").click() + + dash_duo.wait_for_text_to_equal("#output", "done") + error_title = "ID running component not found in layout" + error_message = [ + "Component defined in running keyword not found in layout.", + 'Component id: "non_existent_component"', + "This ID was used in the callback(s) for Output(s):", + "output.children", + "You can suppress this exception by setting", + "`suppress_callback_exceptions=True`.", + ] + # The error should show twice, once for trying to set running on and once for + # turning it off. + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") + for error in dash_duo.find_elements(".dash-fe-error__title"): + assert error.text == error_title + for error_text in dash_duo.find_elements(".dash-backend-error"): + assert all(line in error_text for line in error_message)