bytedance/deer-flow · warning · HTTPException

request config configurable must be an object

Error message

request config configurable must be an object

What it means

HTTP 400 raised when the run's config.configurable is present but not an object — the checkpoint reference is valid, but the request config that must carry thread/checkpoint keys is malformed. The gateway needs a dict to attach thread_id, checkpoint_ns, checkpoint_id, and checkpoint_map into config.configurable.

Source

Thrown at backend/app/gateway/services.py:980

            "checkpoint_ns": checkpoint_ns,
            "checkpoint_id": str(checkpoint_id),
        }
    }
    if checkpoint_map is not None:
        read_config["configurable"]["checkpoint_map"] = checkpoint_map

    checkpointer = get_checkpointer(request)
    try:
        checkpoint_tuple = await checkpointer.aget_tuple(read_config)
    except Exception as exc:
        logger.exception("Failed to validate checkpoint %s for thread %s", checkpoint_id, sanitize_log_param(thread_id))
        raise HTTPException(status_code=500, detail="Failed to validate checkpoint") from exc
    if checkpoint_tuple is None:
        raise HTTPException(status_code=404, detail=f"Checkpoint {checkpoint_id} not found")

    configurable = config.setdefault("configurable", {})
    if not isinstance(configurable, dict):
        raise HTTPException(status_code=400, detail="request config configurable must be an object")
    configurable["thread_id"] = thread_id
    configurable["checkpoint_ns"] = checkpoint_ns
    configurable["checkpoint_id"] = str(checkpoint_id)
    if checkpoint_map is not None:
        configurable["checkpoint_map"] = checkpoint_map


async def ensure_checkpoint_history_seeded(
    request: Request,
    *,
    thread_id: str,
    assistant_id: str | None,
) -> None:
    """Backfill an empty run-event feed from an existing checkpoint head.

    No-op unless the feed is empty AND a checkpoint head with messages
    exists — i.e. a legacy checkpoint-only thread facing its first journaled
    run. This is a migration shim: remove it once pre-journal threads are no

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Make config.configurable a JSON object: {"configurable": {"thread_id": "..."}}.
  2. Or omit config entirely when you have nothing to put in configurable — the gateway sets the required keys itself.

Example fix

# before
{"config": {"configurable": "t1"}, "checkpoint_id": "cp1", "input": {...}}

# after
{"config": {"configurable": {"thread_id": "t1"}}, "checkpoint_id": "cp1", "input": {...}}
Defensive patterns

Strategy: type-guard

Validate before calling

const cfg = payload.config?.configurable;
if (cfg !== undefined && (typeof cfg !== 'object' || cfg === null || Array.isArray(cfg))) {
  throw new TypeError('config.configurable must be an object');
}

Type guard

const isConfigurable = (c: unknown): c is Record<string, unknown> =>
  typeof c === 'object' && c !== null && !Array.isArray(c);

Prevention

When it happens

Trigger: Sending "config": {"configurable": "thread-123"} or configurable: ["a","b"] together with a checkpoint reference.

Common situations: Clients modeling configurable as a string thread id (a plausible shorthand); passing a serialized JSON string of the configurable object; schema confusion between config.configurable and config.metadata.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/74465e5807354f0e. Report an issue: GitHub.