How to read cookies inside a background callback?

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)

  1. Copied your file into new dir
  2. python -m venv venv
  3. .\venv\Scripts\activate.bat
  4. pip install dash[diskcache]
  5. 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.

1 Like