{"record":{"id":"314a656433915b66","repo":"unclecode/crawl4ai","slug":"invalid-or-missing-api-token","errorCode":null,"errorMessage":"Invalid or missing api_token","messagePattern":"Invalid or missing api_token","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"deploy/docker/server.py","lineNumber":545,"sourceCode":"    return JSONResponse(\n        {\"error\": \"Internal server error\", \"correlation_id\": cid},\n        status_code=500,\n    )\n\n\n# ──────────────────────── Endpoints ──────────────────────────\n@app.post(\"/token\")\nasync def get_token(req: TokenRequest):\n    expected_token = config.get(\"security\", {}).get(\"api_token\", \"\")\n    if not expected_token:\n        # Fail closed: without a configured api_token the old behavior minted a\n        # JWT to anyone whose email merely had an MX record. Refuse instead.\n        raise HTTPException(\n            403,\n            \"Token issuance is disabled: no api_token is configured on the server.\",\n        )\n    if not req.api_token or not constant_time_eq(req.api_token, expected_token):\n        raise HTTPException(401, \"Invalid or missing api_token\")\n    if not verify_email_domain(req.email):\n        raise HTTPException(400, \"Invalid email domain\")\n    token = create_access_token({\"sub\": req.email})\n    return {\"email\": req.email, \"access_token\": token, \"token_type\": \"bearer\"}\n\n\n@app.post(\"/config/dump\")\nasync def config_dump(\n    data: dict,\n    _td: Dict = Depends(token_dep),\n):\n    try:\n        return JSONResponse(_config_from_json(data))\n    except (TypeError, ValueError) as e:\n        raise HTTPException(400, str(e))\n\n\n@app.post(\"/md\")","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/deploy/docker/server.py#L527-L563","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"# before\ntoken = os.environ.get('API_TOKEN')  # None -> sent as ''\npost(f\"{base}/token\", json={\"email\": e, \"api_token\": token or \"\"})  # 401\n\n# after\ntoken = os.environ['API_TOKEN'].strip()\npost(f\"{base}/token\", json={\"email\": e, \"api_token\": token})","handlingStrategy":"validation","validationCode":"def has_api_token() -> bool:\n    token = os.environ.get('API_TOKEN', '')\n    return bool(token.strip())","typeGuard":null,"tryCatchPattern":"try:\n    tok = post(f\"{base}/token\", json={'email': email, 'api_token': api_token})\nexcept HTTPError as e:\n    if e.response.status_code == 401:\n        raise PermissionError(\n            'api_token rejected; verify the shared secret matches the server config (rotation?)'\n        ) from e\n    raise","preventionTips":["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."],"tags":["fastapi","authentication","security","token","credentials"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}