Repository: plotly/dash

Issue:
[BUG] PlotlyJSONEncoder cannot serialize dash.dash._NoUpdate responses; throws TypeError
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.


Discussion:
Thanks @ProbonoBonobo - can you post an example callback that shows this behavior? This works fine for me:
```py
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.
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.
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.
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.
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:

https://github.com/plotly/dash/blob/5d9f57812c39fb5b38caa06fc2164b16d34f7951/dash/dash.py#L1360

and here:

https://github.com/plotly/dash/blob/5d9f57812c39fb5b38caa06fc2164b16d34f7951/dash/dash.py#L1370

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!

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -1357,7 +1357,7 @@ def add_context(*args, **kwargs):
                     has_update = False
                     for i, o in enumerate(output):
                         val = output_value[i]
-                        if val is not no_update:
+                        if not isinstance(val, _NoUpdate):
                             has_update = True
                             o_id, o_prop = o.component_id, o.component_property
                             component_ids[o_id][o_prop] = val
@@ -1367,7 +1367,7 @@ def add_context(*args, **kwargs):
 
                     response = {"response": component_ids, "multi": True}
                 else:
-                    if output_value is no_update:
+                    if isinstance(output_value, _NoUpdate):
                         raise exceptions.PreventUpdate
 
                     response = {


Test patch:
diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py
--- a/tests/integration/test_integration.py
+++ b/tests/integration/test_integration.py
@@ -2,6 +2,7 @@
 import datetime
 import time
 import pytest
+from copy import copy
 
 from bs4 import BeautifulSoup
 from selenium.webdriver.common.keys import Keys
@@ -527,7 +528,8 @@ def show_clicks(n):
         return [
             no_update if n and n > 4 else n,
             no_update if n and n > 2 else n,
-            no_update,
+            # make a new instance, to mock up caching and restoring no_update
+            copy(no_update),
         ]
 
     dash_duo.start_server(app)