Skip to content

Commit dfec766

Browse files
committed
validate that callback request "outputs" field matches callback
1 parent 66c8451 commit dfec766

File tree

3 files changed

+81
-1
lines changed

3 files changed

+81
-1
lines changed

dash/_validate.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,26 @@ def validate_id_string(arg):
111111
)
112112

113113

114+
def validate_output_spec(output, output_spec, Output):
115+
"""
116+
This validation is for security and internal debugging, not for users,
117+
so the messages are not intended to be clear.
118+
`output` comes from the callback definition, `output_spec` from the request.
119+
"""
120+
if not isinstance(output, (list, tuple)):
121+
output, output_spec = [output], [output_spec]
122+
elif len(output) != len(output_spec):
123+
raise exceptions.CallbackException("Wrong length output_spec")
124+
125+
for outi, speci in zip(output, output_spec):
126+
speci_list = speci if isinstance(speci, (list, tuple)) else [speci]
127+
for specij in speci_list:
128+
if Output(specij["id"], specij["property"]) != outi:
129+
raise exceptions.CallbackException(
130+
"Output does not match callback definition"
131+
)
132+
133+
114134
def validate_multi_return(outputs_list, output_value, callback_id):
115135
if not isinstance(output_value, (list, tuple)):
116136
raise exceptions.InvalidCallbackReturnValue(

dash/dash.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from .fingerprint import build_fingerprint, check_fingerprint
2929
from .resources import Scripts, Css
30-
from .dependencies import handle_callback_args
30+
from .dependencies import handle_callback_args, Output
3131
from .development.base_component import ComponentRegistry
3232
from .exceptions import PreventUpdate, InvalidResourceError, ProxyError
3333
from .version import __version__
@@ -1004,6 +1004,7 @@ def wrap_func(func):
10041004
@wraps(func)
10051005
def add_context(*args, **kwargs):
10061006
output_spec = kwargs.pop("outputs_list")
1007+
_validate.validate_output_spec(output, output_spec, Output)
10071008

10081009
# don't touch the comment on the next line - used by debugger
10091010
output_value = func(*args, **kwargs) # %% callback invoked %%
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import requests
2+
3+
4+
import dash_core_components as dcc
5+
import dash_html_components as html
6+
import dash
7+
from dash.dependencies import Input, Output
8+
9+
10+
def test_cbmf001_bad_output_outputs(dash_thread_server):
11+
app = dash.Dash(__name__)
12+
app.layout = html.Div(
13+
[
14+
dcc.Input(id="i", value="initial value"),
15+
html.Div(html.Div([1.5, None, "string", html.Div(id="o1")])),
16+
]
17+
)
18+
19+
@app.callback(Output("o1", "children"), [Input("i", "value")])
20+
def update_output(value):
21+
return value
22+
23+
dash_thread_server(app)
24+
25+
# first a good request
26+
response = requests.post(
27+
dash_thread_server.url + "/_dash-update-component",
28+
json=dict(
29+
output="o1.children",
30+
outputs={"id": "o1", "property": "children"},
31+
inputs=[{"id": "i", "property": "value", "value": 9}],
32+
changedPropIds=["i.value"],
33+
),
34+
)
35+
assert response.status_code == 200
36+
assert '"o1": {"children": 9}' in response.text
37+
38+
# now some bad ones
39+
outspecs = [
40+
{"output": "o1.nope", "outputs": {"id": "o1", "property": "nope"}},
41+
{"output": "o1.children", "outputs": {"id": "o1", "property": "nope"}},
42+
{"output": "o1.nope", "outputs": {"id": "o1", "property": "children"}},
43+
{"output": "o1.children", "outputs": {"id": "nope", "property": "children"}},
44+
{"output": "nope.children", "outputs": {"id": "nope", "property": "children"}},
45+
]
46+
for outspeci in outspecs:
47+
response = requests.post(
48+
dash_thread_server.url + "/_dash-update-component",
49+
json=dict(
50+
inputs=[{"id": "i", "property": "value", "value": 9}],
51+
changedPropIds=["i.value"],
52+
**outspeci
53+
),
54+
)
55+
assert response.status_code == 500
56+
assert "o1" not in response.text
57+
assert "children" not in response.text
58+
assert "nope" not in response.text
59+
assert "500 Internal Server Error" in response.text

0 commit comments

Comments
 (0)