Skip to content

[BUG] Background callbacks fail to serialise request args on FastAPI/Quart backends (QueryParams is not JSON serializable) #3816

@i-murray

Description

@i-murray

Background callbacks fail to serialise request args on FastAPI/Quart backends (QueryParams is not JSON serializable)

Summary

When a background=True callback is dispatched on a non-Flask backend (e.g. the FastAPI or Quart adapters), Dash attempts to serialise the callback context to the background worker (Celery via kombu, or Diskcache). Serialisation fails because the request args stored on the context is a backend-specific multi-dict starlette.datastructures.QueryParams on FastAPI, which is not JSON serialisable.

The result is a runtime error such as:

TypeError: Object of type QueryParams is not JSON serializable

(or the equivalent kombu/EncodeError wrapping it) whenever a background callback is triggered. The same problem applies to the Quart adapter, whose args is a werkzeug/quart MultiDict.

Environment

  • Dash version: 4.2.0
  • Backend: FastAPI (also affects Quart)
  • Background callback manager: Celery (CeleryManager); Diskcache is likely affected for the same reason
  • Python: 3.12

Steps to reproduce

  1. Configure a Dash app on the FastAPI backend with a Celery background callback manager.
  2. Define any callback with background=True.
  3. Trigger the callback from the browser.
from dash import Dash, Input, Output, html
from dash.background_callback import CeleryManager
from celery import Celery

celery_app = Celery(__name__, broker=..., backend=...)
manager = CeleryManager(celery_app)

app = Dash(__name__, background_callback_manager=manager)  # FastAPI backend
app.layout = html.Div([
    html.Button("Run", id="run"),
    html.Div(id="out"),
])

@app.callback(
    Output("out", "children"),
    Input("run", "n_clicks"),
    background=True,
    prevent_initial_call=True,
)
def slow(n):
    return f"done {n}"

Clicking Run dispatches the job and raises the serialisation error.

Expected behaviour

A background callback should be dispatched to the worker successfully, regardless of which backend (Flask, FastAPI, Quart) is in use. The request context handed to the worker must be JSON serialisable.

Actual behaviour

Dispatch fails because the callback context contains a non-serialisable args object.

Root cause analysis

During request setup, Dash._initialize_context stores the request data on the context. Unlike cookies and headers, which are explicitly coerced to plain dicts, args is stored as-is:

dash/dash.py (_initialize_context):

g.cookies = dict(adapter.cookies)
g.headers = dict(adapter.headers)
g.args = adapter.args            # <-- not coerced to a plain dict

This is intentional for the synchronous HTTP path: several code paths rely on the multi-dict interface, e.g. callback_context.args.getlist("cancelJob") and adapter.args.getlist("oldJob"), which a plain dict does not provide.

On the FastAPI backend, adapter.args is the raw Starlette query params:

dash/backends/_fastapi.py:

@property
def args(self):
    return self._request.query_params   # starlette.datastructures.QueryParams

For background=True callbacks, _setup_background_callback (dash/_callback.py) copies the context value and dispatches it to the worker via callback_manager.call_job_fn(...). The background callback manager serialises this context (Celery → kombu → JSON), and serialisation fails on the QueryParams (FastAPI) / MultiDict (Quart) object.

On the Flask backend the issue does not surface in the same way because the worker receives a JSON-serialisable representation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions