Hi,
I’m making some http requests in my dash callbacks. Some of these requests could be slow, so I decided to turn corresponding callbacks into background callbacks.
In order to make proper network requests I need a cookie that is set in user’s browser. Currently I take it from flask.request.cookies
which probably refers to a request made from browser to Dash.
However, when I use the same code inside a background callback, I get a flask error Working outside of request context
. I assume this is reasonable as a background callback is executed somewhere in the background, thus actually outside of request context.
Obviously, I can store the cookie value in a dcc.Store but this would require changing all my callbacks definitions, adding a new dependency. The same should be done for any new callback.
I’ve tried to somehow read cookies from callback_context, as it is available in background callbacks, but it only has a response
which allows to set a response cookie, but not to read a request cookie.
Does anyone have any ideas of how can I easily read a cookie inside a background callback?
Hello @Exelenz9,
Welcome to the community!
Just curious, have you tried doing this in your callback?
def bgCallback(var1, var2, request=flask.request):
I haven’t tried this, but am wondering if the would work.
Hi @jinnyzor
Thanks for a quick reply!
What is a request=
here? Is it a callback dependency with a default value? Then I guess this is almost the same as storing a cookie in dcc.Store as I still need to update every callback definition.
Maybe I missed to mention one thing. Currently all my API calls from callback use some common wrapper inside which does the job of taking a cookie from flask.request
. What I want, is to change this common code somehow, so any of my callbacks could work as a background ones with no additional changes (apart from background=True
, of course)
The difference between the store is that you’d have to store it, this just sends the request along with the initial function call.
You may be able to loop through the callbacks and add this default to all background callbacks at dash initialization.
Hello @Exelenz9,
Do you have a specific setup for the caching that you are using? Do you happen to be using dash_extensions?
If possible, can you post a little bit of your code?
That makes sense. Maybe I’ll try something like this. Thanks!
Do you mean caching to enable background callbacks? For now I simply use some default diskcache config
Just wondering if you had any other packages other than dash, like dash_extensions.
Btw, I tested it with and without the declaration in regular dash and I didn’t have an issue seeing the flask.request.
No, I don’t use dash_extensions. Why do you ask?
I also tested this approach yesterday, and it doesn’t work for me. If I specify request=flask.request
as a default new argument value, I still get an error when I call request.cookies
inside my background callback as this operation is performed outside of the request context:
@app.callback(
Output("pre-to-test", "children"),
Input("pre-to-test", "className"),
background=True,
)
def to_test(class_name, request=flask.request):
a = request.cookies # <- Working outside of request context
return json.dumps(a)
If I specify the default argument value as flask.request.cookies
, this obviously also doesn’t work as this code is executed on app startup, which is not a request context as well:
@app.callback(
Output("pre-to-test", "children"),
Input("pre-to-test", "className"),
background=True,
)
def to_test(
class_name,
cookies=flask.request.cookies # <- Working outside of request context
):
a = cookies
return json.dumps(a)
I assume, I need a time when the browser makes a request to Dash and Dash is about to forward an action to background callback executor (diskcache in my case). Do you think updating the callback_context
, similar to this change, is a good idea? Like adding a request
next to response
Are you using celery as your background manager?
I was just curious, I know dash_extensions removes the ability for ctx sometimes.
Are you using celery as your background manager?
Probably I will use Celery later, but for now I use DiskCache
I was using diskcache and I was able to print the request… hmm.
Try running this:
import time
import os
import dash
from dash import DiskcacheManager, CeleryManager, Input, Output, html
import flask
# Diskcache for non-production apps when developing locally
import diskcache
cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)
app = dash.Dash(__name__, background_callback_manager=background_callback_manager)
app.layout = html.Div(
[
html.Div([html.P(id="paragraph_id", children=["Button not clicked"])]),
html.Button(id="button_id", children="Run Job!"),
]
)
@dash.callback(
output=Output("paragraph_id", "children"),
inputs=Input("button_id", "n_clicks"),
background=True,
manager=background_callback_manager,
)
def update_clicks(n_clicks, request=flask.request):
time.sleep(2)
return [f"Clicked {n_clicks} times, {request.headers}"]
if __name__ == "__main__":
app.run_server(debug=True, port=12345)
I can see my headers. Also, what dash version are you using? I am using 2.7.0
Is this the exact code that you run?
What are the flask and python versions that you use?
I’ve copied your code, switched to dash 2.7.0, flask 2.2.2, python 3.10.7, but now I’m getting an error when trying to set flask.request
as a default argument value:
_pickle.PicklingError: args[0] from __newobj__ args has the wrong class
Flask is whatever 2.7 downloads.
3.9 python.
And yes, that is the exact code.
And which OS?
I’ve created a completely new project on two different PCs (both Windows)
- Copied your file into new dir
python -m venv venv
.\venv\Scripts\activate.bat
pip install dash[diskcache]
python run.py
Still the same PicklingError
.
Weird, I ran it on mac, hold on.
Ok… 3 hours later, lol.
It seems like windows has an issue pickling the request, thus it cant handle it… However, this is progress:
import time
import os
import dash
from dash import DiskcacheManager, CeleryManager, Input, Output, html
import flask
import traceback, json
# Diskcache for non-production apps when developing locally
import diskcache, sqlite3
cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)
server = flask.Flask(__name__)
@server.before_request
def printRequest():
request = flask.request
if request.method == 'POST' and '/_dash-update-component' == request.path and len(str(request.url).split('?'))==1:
requestsCaching(request)
app = dash.Dash(__name__, server=server, background_callback_manager=background_callback_manager)
app.layout = html.Div(
[
html.Div([html.P(id="paragraph_id", children=["Button not clicked"])]),
html.Button(id="button_id", children="Run Job!"),
]
)
caching = []
def requestsCaching(request = None):
requestCache = sqlite3.connect('./cache/requestCache.db')
requestCache.cursor().execute('CREATE TABLE IF NOT EXISTS requests (requests)')
if request:
strSQL = "insert into requests(requests) values(?)"
requestCache.cursor().execute(strSQL, [json.dumps(dict(request.headers))])
requestCache.commit()
else:
r = json.loads(requestCache.cursor().execute('select requests from requests order by rowid desc limit 1').fetchone()[0])
return r
@dash.callback(
output=Output("paragraph_id", "children"),
inputs=Input("button_id", "n_clicks"),
background=True,
manager=background_callback_manager,
prevent_initial_call=True,
)
def update_clicks(n_clicks):
time.sleep(2)
request = requestsCaching()
return [f"Clicked {n_clicks} times, {request}"]
if __name__ == "__main__":
app.run_server(debug=True, port=12345)
Obviously, you’ll want to add some parameters around this to make sure you only use the request that you are trying to get.
2 Likes
Hi @jinnyzor
Thank you for the idea and apologies for the delay.
I realized that my requests headers contain sensitive private data, like unique user auth token and I don’t want to store it in any cache (as this would allow one to get access to it). Also, I have multiple workers and many users so I can’t guarantee that simply extracting the latest row from cache DB would work right.
Probably, I’ll think on something completely different or do not use background callbacks for cases that require cookies at all.
Anyway, thank you for the help!
1 Like
I totally understand.
You can also delete and use an id for the specific request and header association. Still way more tricky…
My strategy was to chain a callback - see Part 3. Basic Callbacks | Dash for Python Documentation | Plotly
I used the first callback with my triggers, and the output of this callback is a dcc.Store which holds my cookie value I wanted to pass through.
My second callback which is the background callback has an input of this store, see notes on implementing callbacks from a dcc.Store Store | Dash for Python Documentation | Plotly