Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] PlotlyJSONEncoder cannot serialize dash.dash._NoUpdate responses; throws TypeError #1014

Closed
ProbonoBonobo opened this issue Nov 15, 2019 · 9 comments

Comments

@ProbonoBonobo
Copy link

Environment:

dash                      1.6.1    
dash-bootstrap-components 0.7.2    
dash-core-components      1.5.1    
dash-daq                  0.3.0    
dash-flexbox-grid         0.2.1    
dash-html-components      1.0.2    
dash-renderer             1.2.1    
dash-table                4.5.1   

Not sure if it's related to the issue I was trying to diagnose, but I noticed the following error printed repeatedly to my console while investigating why my application spends longer (~8x longer) awaiting 204 (No Content) responses than 200 (OK) ones:

Exception on /_dash-update-component [POST]
Traceback (most recent call last):
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1328, in add_context
    response, cls=plotly.utils.PlotlyJSONEncoder
  File "/home/kz/.local/pyenv/versions/3.7.3/lib/python3.7/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/_plotly_utils/utils.py", line 44, in encode
    encoded_o = super(PlotlyJSONEncoder, self).encode(o)
  File "/home/kz/.local/pyenv/versions/3.7.3/lib/python3.7/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/home/kz/.local/pyenv/versions/3.7.3/lib/python3.7/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/_plotly_utils/utils.py", line 113, in default
    return _json.JSONEncoder.default(self, obj)
  File "/home/kz/.local/pyenv/versions/3.7.3/lib/python3.7/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type _NoUpdate is not JSON serializable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/app.py", line 2446, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/app.py", line 1951, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/app.py", line 1820, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/app.py", line 1949, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/flask/app.py", line 1935, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1404, in dispatch
    response.set_data(self.callback_map[output]["callback"](*args))
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1331, in add_context
    self._validate_callback_output(output_value, output)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1187, in _validate_callback_output
    _validate_value(output_value)
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1180, in _validate_value
    toplevel=True,
  File "/home/kz/Envs/dash_bleedingedge/lib/python3.7/site-packages/dash/dash.py", line 1125, in _raise_invalid
    bad_val=bad_val,
dash.exceptions.InvalidCallbackReturnValue: 
The callback for `<Output `approot.children`>`
returned a value having type `_NoUpdate`
which is not JSON serializable.

The value in question is either the only value returned,
or is in the top level of the returned list,
and has string representation
`<dash.dash._NoUpdate object at 0x7ff2ca37ccf8>`

In general, Dash properties can only be
dash components, strings, dictionaries, numbers, None,
or lists of those.

Before filing this as a bug, I considered a possibility that this might be expected behavior. If a _NoUpdate object means the client state is already correct, it might make sense to crash and burn on the encoding, catch the TypeError, and return a 204 response header immediately.

But I'm pretty sure this is just a boring, garden-variety serialization bug, because (as I understand it) it's perfectly alright to sandwich _NoUpdate values between actual data in the return value of a multi-output callback. So I would expect PlotlyJSONEncoder to have a mechanism for serializing those objects.

@alexcjohnson
Copy link
Collaborator

Thanks @ProbonoBonobo - can you post an example callback that shows this behavior? This works fine for me:

app.layout = html.Div([
    dcc.Input(id='x'),
    html.P('aaaaa', id='a'),
    html.P('bbbbb', id='b'),
    html.P('ccccc', id='c')
])

@app.callback(
    [Output('a', 'children'), Output('b', 'children'), Output('c', 'children')],
    [Input('x', 'value')]
)
def f(x):
    return x, dash.no_update, (x or '') + 'c'

The dash.no_update object is normally stripped out before we try to serialize the response - we simply don't include an entry for outputs that don't get updated, and the response only becomes 204 if there are NO updates.

@ProbonoBonobo
Copy link
Author

My hunch is that this error emerges in combination with flask-caching, in particular the case where the return value of a callback function is memoized. This hunch is supported by my observations that the error presents only when observed properties are modified in quick succession. When those requests are received at lengthier intervals than the cache timeout, I see no such errors. But I will want to confirm that with a minimal reproducible example first. Will try to make one ASAP.

@ProbonoBonobo
Copy link
Author

ProbonoBonobo commented Nov 16, 2019

Yep, it's related to flask_caching. Here is the example app from the documentation that explains how to integrate Redis into an existing Dash app, which I've modified slightly to reproduce the error:

import datetime
import os
import random
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from flask_caching import Cache
from dash import no_update
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
cache = Cache(app.server, config={
    'CACHE_TYPE': 'filesystem',
    'CACHE_DIR': "/tmp"
})
app.config.suppress_callback_exceptions = True
server = app.server


timeout = 20
app.layout = html.Div([
    html.Div(id='flask-cache-memoized-children'),
    dcc.RadioItems(
        id='flask-cache-memoized-dropdown',
        options=[
            {'label': 'Option {}'.format(i), 'value': 'Option {}'.format(i)}
            for i in range(1, 4)
        ],
        value='Option 1'
    ),
    html.Div('Results are cached for {} seconds'.format(timeout))
])


@app.callback(
    Output('flask-cache-memoized-children', 'children'),
    [Input('flask-cache-memoized-dropdown', 'value')])
@cache.memoize(timeout=timeout)  # in seconds
def render(value):
    # Rather than always returning a string, suppose we intermittently return the dash.no_update noop instead 
    return random.choice(['Selected "{}" at "{}"'.format(
        value, datetime.datetime.now().strftime('%H:%M:%S')
    ), no_update])

For this example I'm using the filesystem instead of redis to make the code easier to run, but the caching backend makes no difference (I can confirm it happens with redis too). The error relates to the @cache.memoize function itself somehow. It looks like it's thrown whenever: (1) an incoming request is a cache hit; and (2) the cached response contains a dash.no_update object. (So to reproduce, you'll need to click back and forth between the RadioButton choices a few times and wait for Flask to return a cached no_update response before it expires.)

Commenting out the @cache.memoize(timeout=timeout) line suppresses the error, so I think we have the culprit.

@ProbonoBonobo
Copy link
Author

ProbonoBonobo commented Nov 16, 2019

Oh, one more important detail I didn't realize until just now: I'm initializing the application server with gunicorn --workers 4 caching:server --bind=127.0.0.1:8050 --keep-alive 10 --worker-class eventlet but gunicorn caching:server --bind=127.0.0.1:8050 is sufficient to trigger it. When I use the standard dash.Dash.run_server method there is no error printed to console. This is just a wild guess, but I'm wondering if the dash.server instance is catching some of PlotlyJSONEncoder's errors and transforming the response appropriately? Because if so, it seems likely to me that what's happening is that a serialization error is fired while attempting to encode an HTTP 200 response payload because gunicorn doesn't know to transform the response header's status code to 204 when passed a no_update object from the cache.

@alexcjohnson
Copy link
Collaborator

Ah OK, I guess that makes sense - when no_update comes back from the cache it must be a new copy of _NoUpdate, so our checks assuming it's a singleton will fail to catch it and it'll remain in the response. That's here:

dash/dash/dash.py

Line 1360 in 5d9f578

if val is not no_update:

and here:

dash/dash/dash.py

Line 1370 in 5d9f578

if output_value is no_update:

We should be able to just replace those checks by isinstance(val, _NoUpdate). Then as a test, just make a new copy of _NoUpdate and return that from a callback, verify that we get the correct response.

Thanks for the help debugging this!

alexcjohnson added a commit that referenced this issue Dec 10, 2019
fix #1014 - test for no_update by type rather than identity
@rodrigoazevedo07
Copy link

Hi, I'm new on Dash and sorry if I'm using this comment in a inappropriate way. But, I'm experimenting this error in my app, since I'd upgraded the dash version from 1.9.3 to 1.12.0. My callback return just contain value, options and data (dcc.store) property types. So, is the bug solved in that last version? If not, is there a workaround in the meantime? Thanks.

@JonThom
Copy link

JonThom commented Oct 18, 2021

Hi,
I'm getting the same error with Dash 2.0.0.
@rodrigoazevedo07 did you ever find a solution?
@alexcjohnson @ProbonoBonobo any ideas why?
I can set up a reproducible example later this week if useful.

EDIT: It seems that this error was actually caused by (unintentionally) returning a 'dict_keys' object one of multiple Outputs, where other Outputs include dash.no_update.

However, although the 'dict_keys' object appears to have been the culprit, the exception was the same as the one in this issue, i.e. TypeError: Object of type _NoUpdate is not JSON serializable.

This was very confusing! Any idea why this would happen?

@alexcjohnson
Copy link
Collaborator

@JonThom I don't know offhand why dict_keys would lead to that error message, but if you want to post a new issue (it'll get lost if posted in this closed issue) with a reproducible example we'd be happy to investigate - certainly not the debugging experience we want!

@rodrigoazevedo07
Copy link

Hi, I'm getting the same error with Dash 2.0.0. @rodrigoazevedo07 did you ever find a solution? @alexcjohnson @ProbonoBonobo any ideas why? I can set up a reproducible example later this week if useful.

EDIT: It seems that this error was actually caused by (unintentionally) returning a 'dict_keys' object one of multiple Outputs, where other Outputs include dash.no_update.

However, although the 'dict_keys' object appears to have been the culprit, the exception was the same as the one in this issue, i.e. TypeError: Object of type _NoUpdate is not JSON serializable.

This was very confusing! Any idea why this would happen?

Hi JonaThan, i can't remember exactly what i did, but I worked around the problem by doing a refactor of my app, including a dash upgrade. Sorry if I couldn't help you out more. It's been a while since the last time i worked on that code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants