unclecode/crawl4ai · error · HTTPException
str(e)
Error message
str(e)
What it means
A 400 raised by POST /config/dump when _config_from_json() raises TypeError or ValueError while parsing the submitted config dict. The detail is the underlying exception message, so the real cause (bad key, wrong type, malformed value) is embedded in str(e).
Source
Thrown at deploy/docker/server.py:560
"Token issuance is disabled: no api_token is configured on the server.",
)
if not req.api_token or not constant_time_eq(req.api_token, expected_token):
raise HTTPException(401, "Invalid or missing api_token")
if not verify_email_domain(req.email):
raise HTTPException(400, "Invalid email domain")
token = create_access_token({"sub": req.email})
return {"email": req.email, "access_token": token, "token_type": "bearer"}
@app.post("/config/dump")
async def config_dump(
data: dict,
_td: Dict = Depends(token_dep),
):
try:
return JSONResponse(_config_from_json(data))
except (TypeError, ValueError) as e:
raise HTTPException(400, str(e))
@app.post("/md")
@limiter.limit(config["rate_limiting"]["default_limit"])
@mcp_tool("md")
async def get_markdown(
request: Request,
body: MarkdownRequest,
_td: Dict = Depends(token_dep),
):
"""
Convert a web page into Markdown format.
Supports multiple extraction modes:
- fit (default): Readability-based extraction for clean content
- raw: Direct DOM to Markdown conversion
- bm25: BM25 relevance ranking with optional query
- llm: LLM-based summarization with optional queryView on GitHub (pinned to 7e80152142)
Solutions
- Read the str(e) detail in the 400 response — it names the offending key/value.
- Validate your payload against the current schema first: GET /schema returns the canonical BrowserConfig/CrawlerRunConfig dump.
- Regenerate the config from a working crawl instead of hand-writing it.
- Match the crawl4ai version between the client that produced the config and the server that consumes it.
Example fix
# before
requests.post(f'{base}/config/dump', json={'browser': {'headless': 'yes'}}) # str instead of bool
# after
schema = requests.get(f'{base}/schema').json()
requests.post(f'{base}/config/dump', json={'browser': {'headless': True}}) Defensive patterns
Strategy: validation
Validate before calling
import requests
schema = requests.get(f'{BASE}/schema').json() # canonical config shape
def check_config(cfg: dict) -> None:
def walk(node, template, path='config'):
if isinstance(template, dict):
for k, v in node.items():
if k not in template:
raise ValueError(f'unknown key {path}.{k}')
walk(v, template[k], f'{path}.{k}')
elif isinstance(template, bool):
if not isinstance(node, bool):
raise ValueError(f'{path} must be bool, got {type(node).__name__}')
walk(cfg, schema) Try / catch
resp = requests.post(f'{BASE}/config/dump', json=cfg, headers=hdrs)
if resp.status_code == 400:
raise ValueError(f'config rejected: {resp.json()["detail"]}') # detail carries the offending key Prevention
- Fetch /schema and validate keys/types before POSTing a config.
- Never hand-edit config dumps; derive them from a successful crawl.
- Pin the crawl4ai version so config schema doesn't drift under you.
When it happens
Trigger: POST /config/dump with a JSON body that is not a valid CrawlerRunConfig/BrowserConfig shape: unknown/misspelled keys, wrong value types (string where a number is expected), or nested structures that fail pydantic/manual parsing. Auth via token_dep must already have passed.
Common situations: Client sends a config dumped from an older/newer crawl4ai version whose schema differs; hand-editing a config dump and introducing typos; passing {'crawler': {...}} vs the flat dict the endpoint expects.
Related errors
- invalid params for hook '{action}': {e}
- Rejected config: {e}
- Container not found: ${config.container_selector}
- Invalid source(s): {invalid}. Valid: {VALID_SOURCES}
- str(e)
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/22215011de4f3c62.
Report an issue: GitHub.