unclecode/crawl4ai · error · HTTPException
Invalid or missing api_token
Error message
Invalid or missing api_token
What it means
An explicit 401 raised by POST /token when the submitted api_token is empty or does not constant-time-equal the server's configured security.api_token. The comparison uses constant_time_eq, so timing side-channels are closed; a mismatch simply means the shared secret is wrong or missing from the request body.
Source
Thrown at deploy/docker/server.py:545
return JSONResponse(
{"error": "Internal server error", "correlation_id": cid},
status_code=500,
)
# ──────────────────────── Endpoints ──────────────────────────
@app.post("/token")
async def get_token(req: TokenRequest):
expected_token = config.get("security", {}).get("api_token", "")
if not expected_token:
# Fail closed: without a configured api_token the old behavior minted a
# JWT to anyone whose email merely had an MX record. Refuse instead.
raise HTTPException(
403,
"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")View on GitHub (pinned to 7e80152142)
Solutions
- Send the exact configured security.api_token in the request body's api_token field - non-empty and byte-identical.
- If the token was rotated, distribute the new value through the secret store and redeploy clients.
- Strip whitespace/newlines when loading the token from env files, and confirm which environment's token the server expects (staging vs production).
Example fix
# before
token = os.environ.get('API_TOKEN') # None -> sent as ''
post(f"{base}/token", json={"email": e, "api_token": token or ""}) # 401
# after
token = os.environ['API_TOKEN'].strip()
post(f"{base}/token", json={"email": e, "api_token": token}) Defensive patterns
Strategy: validation
Validate before calling
def has_api_token() -> bool:
token = os.environ.get('API_TOKEN', '')
return bool(token.strip()) Try / catch
try:
tok = post(f"{base}/token", json={'email': email, 'api_token': api_token})
except HTTPError as e:
if e.response.status_code == 401:
raise PermissionError(
'api_token rejected; verify the shared secret matches the server config (rotation?)'
) from e
raise Prevention
- Load the api_token from the secret store and .strip() it before sending.
- After server-side token rotation, update all clients/CI environments in the same change window.
- Fail fast at client startup when the token env var is missing, instead of sending empty credentials.
When it happens
Trigger: Calling POST /token with api_token omitted, empty, stale (rotated server-side), copied with whitespace/newline artifacts, or from an environment holding a different value than the server's config.
Common situations: Secret rotated on the server but clients still hold the old value; CI/CD pipelines missing the token env var so requests send ''; copy-paste introducing trailing newlines; mismatch between staging and production tokens.
Related errors
- Token issuance is disabled: no api_token is configured on th
- URL must start with {schemes}
- type must be 'CrawlerRunConfig' or 'BrowserConfig'
- Invalid wait_for parameter: '{wait_for}'. It should be eithe
- [NSTProxy] token and channel_id are required
AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14).
Data as JSON: /api/errors/314a656433915b66.
Report an issue: GitHub.