{"record":{"id":"3624abffca536155","repo":"unclecode/crawl4ai","slug":"type-must-be-crawlerrunconfig-or-browserconfig","errorCode":null,"errorMessage":"type must be 'CrawlerRunConfig' or 'BrowserConfig'","messagePattern":"type must be 'CrawlerRunConfig' or 'BrowserConfig'","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"warning","filePath":"deploy/docker/server.py","lineNumber":477,"sourceCode":"}\n\n\ndef _config_from_json(data: dict) -> dict:\n    \"\"\"Validate a {type, params} config under the untrusted trust boundary and\n    echo the normalized result.\n\n    This endpoint is no longer a gadget-construction oracle: only the gated,\n    side-effect-free CrawlerRunConfig/BrowserConfig types may be validated, the\n    untrusted gate raises on forbidden power-fields and disallowed nested types\n    (LLM*, proxy, deep-crawl - which is what would read env/secrets), drops\n    unknown fields, and clamps quantities.\"\"\"\n    config_type = data.get(\"type\")\n    if config_type == \"CrawlerRunConfig\":\n        obj = CrawlerRunConfig.load(data, provenance=Provenance.UNTRUSTED)\n    elif config_type == \"BrowserConfig\":\n        obj = BrowserConfig.load(data, provenance=Provenance.UNTRUSTED)\n    else:\n        raise ValueError(\"type must be 'CrawlerRunConfig' or 'BrowserConfig'\")\n    return obj.dump()\n\n\n# ── job router ──────────────────────────────────────────────\napp.include_router(init_job_router(redis, config, token_dep))\n\n# ── monitor router ──────────────────────────────────────────\n# Do not attach token_dep at router level: it is HTTP Request-only and breaks\n# the WebSocket upgrade on /monitor/ws (TypeError: _principal() missing 'request').\n# AuthGateMiddleware already authenticates HTTP + WS; destructive monitor\n# actions keep their own Depends(require_admin).\nfrom monitor_routes import router as monitor_router\napp.include_router(monitor_router)\n\nlogger = logging.getLogger(__name__)\n\n\n# ── central exception handling (no internal detail leaks) ─────────────","sourceCodeStart":459,"sourceCodeEnd":495,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/deploy/docker/server.py#L459-L495","documentation":"A ValueError raised by _config_from_json() in the crawl server when the 'type' field of a submitted {type, params} config payload is anything other than the exact strings 'CrawlerRunConfig' or 'BrowserConfig'. The helper is a hardened config-dump validator: only these two gated, side-effect-free types may be constructed; the untrusted loader additionally rejects power-fields (LLM*, proxy, deep-crawl), drops unknown fields, and clamps quantities.","triggerScenarios":"POSTing to /config/dump or a crawl endpoint's config slot with {\"type\": \"AsyncPlaywrightCrawlerStrategy\"}, {\"type\": \"crawler\"}, {\"type\": \"LLMConfig\"}, a missing 'type' key (None), or wrong casing like 'crawlerrunconfig'.","commonSituations":"Clients ported from an older API that accepted arbitrary crawl4ai class names; attempts to smuggle in LLM or proxy configuration through the config slot (correctly rejected); copy-paste typos and casing mismatches in hand-written payloads.","solutions":["Set type to exactly 'CrawlerRunConfig' or 'BrowserConfig' (case-sensitive) and put settings under the params field.","Do not attempt to configure LLM, proxy, or deep-crawl behavior through this endpoint - the untrusted gate strips/rejects those by design; use server-side configuration channels instead.","Validate the payload shape client-side against the two allowed types before submitting."],"exampleFix":"# before\n{\"type\": \"crawler_config\", \"params\": {\"word_count_threshold\": 200}}  # ValueError\n\n# after\n{\"type\": \"CrawlerRunConfig\", \"params\": {\"word_count_threshold\": 200}}","handlingStrategy":"type-guard","validationCode":"ALLOWED_CONFIG_TYPES = {'CrawlerRunConfig', 'BrowserConfig'}\n\ndef valid_config_payload(data: dict) -> bool:\n    return (\n        isinstance(data, dict)\n        and data.get('type') in ALLOWED_CONFIG_TYPES\n        and isinstance(data.get('params', {}), dict)\n    )","typeGuard":"from typing import TypeGuard, Literal, TypedDict\n\nConfigType = Literal['CrawlerRunConfig', 'BrowserConfig']\n\nclass ConfigPayload(TypedDict):\n    type: ConfigType\n    params: dict\n\ndef is_config_payload(v) -> TypeGuard[ConfigPayload]:\n    return (\n        isinstance(v, dict)\n        and v.get('type') in ('CrawlerRunConfig', 'BrowserConfig')\n        and isinstance(v.get('params', {}), dict)\n    )","tryCatchPattern":"try:\n    result = client.post('/config/dump', json=payload)\nexcept (ValueError, HTTPError) as e:\n    raise ValueError(\n        f\"config type must be 'CrawlerRunConfig' or 'BrowserConfig'; got {payload.get('type')!r}\"\n    ) from e","preventionTips":["Build config payloads from a client enum limited to the two allowed type strings (case-sensitive).","Never send LLM/proxy/deep-crawl settings through this endpoint - the untrusted gate rejects them by design.","Validate payload shape client-side before every submit; unknown fields are dropped silently, so typo'd params produce no error and no effect."],"tags":["fastapi","validation","security","configuration","input-hardening"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}