Repository: plotly/dash

Issue:
Set a cookie from callback function
It seems that setting a cookies from callback function is currently impossible. I think it would be great if this feature will appear in the future versions.

Thank you:)



Discussion:
My immediate thought was that this could be done with a callback that inserts a <script> tag with some JavaScript that sets a cookie, but according to [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), the html5 spec says that <script> tags inserted via innerHTML should not execute. So no go there.

There are other ways to get arbitrary JS code to execute via innerHTML, which might work (the same MDN article provides an example of how), but at that point it's definitely a hack job. 
Thank you for response!

Interesting and useful workaround. But despite the fact it's a hack job, setting `httponly` cookie couldn't be done this way. 
I suspect this could be a custom Dash component.

What's the use case that you have in mind?

Resolving patch:
diff --git a/dash/_callback_context.py b/dash/_callback_context.py
--- a/dash/_callback_context.py
+++ b/dash/_callback_context.py
@@ -9,9 +9,10 @@ def has_context(func):
     def assert_context(*args, **kwargs):
         if not flask.has_request_context():
             raise exceptions.MissingCallbackContextException(
-                'dash.callback.{} is only available from a callback!'.format(
-                    getattr(func, '__name__')
-                )
+                (
+                    'dash.callback_context.{} '
+                    'is only available from a callback!'
+                ).format(getattr(func, '__name__'))
             )
         return func(*args, **kwargs)
     return assert_context
@@ -33,3 +34,8 @@ def states(self):
     @has_context
     def triggered(self):
         return getattr(flask.g, 'triggered_inputs', [])
+
+    @property
+    @has_context
+    def response(self):
+        return getattr(flask.g, 'dash_response')
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -1023,10 +1023,7 @@ def add_context(*args, **kwargs):
                     '''.format(property=output.component_property,
                                id=output.component_id))
 
-                return flask.Response(
-                    jsonResponse,
-                    mimetype='application/json'
-                )
+                return jsonResponse
 
             self.callback_map[callback_id]['callback'] = add_context
 
@@ -1056,6 +1053,9 @@ def dispatch(self):
             for x in changed_props
         ] if changed_props else []
 
+        response = flask.g.dash_response = flask.Response(
+            mimetype='application/json')
+
         for component_registration in self.callback_map[output]['inputs']:
             args.append([
                 c.get('value', None) for c in inputs if
@@ -1070,7 +1070,8 @@ def dispatch(self):
                 c['id'] == component_registration['id']
             ][0])
 
-        return self.callback_map[output]['callback'](*args)
+        response.set_data(self.callback_map[output]['callback'](*args))
+        return response
 
     def _validate_layout(self):
         if self.layout is None:


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -823,6 +823,32 @@ def update_output(value):
 
         self.percy_snapshot(name='request-hooks interpolated')
 
+    def test_modified_response(self):
+        app = dash.Dash(__name__)
+        app.layout = html.Div([
+            dcc.Input(id='input', value='ab'),
+            html.Div(id='output')
+        ])
+
+        @app.callback(Output('output', 'children'), [Input('input', 'value')])
+        def update_output(value):
+            dash.callback_context.response.set_cookie(
+                'dash cookie', value + ' - cookie')
+            return value + ' - output'
+
+        self.startServer(app)
+        self.wait_for_text_to_equal('#output', 'ab - output')
+        input1 = self.wait_for_element_by_id('input')
+
+        input1.send_keys('cd')
+
+        self.wait_for_text_to_equal('#output', 'abcd - output')
+        cookie = self.driver.get_cookie('dash cookie')
+        # cookie gets json encoded
+        self.assertEqual(cookie['value'], '"abcd - cookie"')
+
+        assert_clean_console(self)
+
     def test_late_component_register(self):
         app = dash.Dash()
 
@@ -954,5 +980,6 @@ def on_click(*args):
                 )
 
     def test_no_callback_context(self):
-        with self.assertRaises(MissingCallbackContextException):
-            no_context = dash.callback_context.inputs
+        for attr in ['inputs', 'states', 'triggered', 'response']:
+            with self.assertRaises(MissingCallbackContextException):
+                getattr(dash.callback_context, attr)