{"record":{"id":"265423dcfc88c7f1","repo":"unclecode/crawl4ai","slug":"invalid-params-for-hook-action-e","errorCode":null,"errorMessage":"invalid params for hook '{action}': {e}","messagePattern":"invalid params for hook '(.+?)': (.+?)","errorType":"validation","errorClass":"HookValidationError","httpStatus":400,"severity":"error","filePath":"deploy/docker/hook_registry.py","lineNumber":219,"sourceCode":"    \"\"\"\n    if not specs:\n        return {}\n    if len(specs) > 10:\n        raise HookValidationError(\"too many hooks (max 10)\")\n\n    grouped: Dict[str, List[Callable]] = {}\n    for spec in specs:\n        action = spec.get(\"action\") if isinstance(spec, dict) else getattr(spec, \"action\", None)\n        raw_params = (spec.get(\"params\", {}) if isinstance(spec, dict) else getattr(spec, \"params\", {})) or {}\n        entry = HOOK_REGISTRY.get(action)\n        if entry is None:\n            raise HookValidationError(\n                f\"unknown hook action {action!r}; allowed: {sorted(HOOK_REGISTRY)}\"\n            )\n        try:\n            params = entry[\"params_model\"](**raw_params)\n        except Exception as e:\n            raise HookValidationError(f\"invalid params for hook '{action}': {e}\")\n        sub_hook = entry[\"factory\"](params)\n        grouped.setdefault(entry[\"hook_point\"], []).append(sub_hook)\n\n    hooks: Dict[str, Callable] = {}\n    for hook_point, sub_hooks in grouped.items():\n        def _compose(sub_hooks):\n            async def composed(page, **kwargs):\n                for fn in sub_hooks:\n                    await fn(page, **kwargs)\n                return page\n            return composed\n        hooks[hook_point] = _compose(sub_hooks)\n    return hooks\n\n\ndef describe_registry() -> dict:\n    \"\"\"Enumerate the available declarative actions for /hooks/info.\"\"\"\n    return {","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/deploy/docker/hook_registry.py#L201-L237","documentation":"build_declarative_hooks wraps pydantic validation of each spec's params: if entry['params_model'](**raw_params) raises (missing required field, out-of-range value, unknown field, or one of the field_validator errors like bad resource types or header checks), it re-raises as HookValidationError(\"invalid params for hook '<action>': <pydantic message>\"). The inner message identifies the exact field problem.","triggerScenarios":"Omitting a required field (e.g. block_resources without resource_types); out-of-range numbers (wait_for_timeout timeout_ms > _MAX_WAIT_MS, scroll_to_bottom delay_ms > _MAX_SCROLL_DELAY_MS); wrong types (string where int expected); passing fields the params model does not define.","commonSituations":"Hand-written JSON configs with typos or missing keys; defaults assumed from a different version; unit or boundary values exceeding the declared ge/le bounds.","solutions":["Parse the pydantic detail after the colon — it names the field and the constraint (e.g. 'timeout_ms: Input should be at most ...').","Re-check the params schema for that action in hook_registry.py (the *_Params model) and conform the spec.","Validate specs client-side with the same constraints before submitting the crawl job."],"exampleFix":"# before\n{\"action\": \"wait_for_timeout\", \"params\": {\"timeout_ms\": 999999999}}\n\n# after\n{\"action\": \"wait_for_timeout\", \"params\": {\"timeout_ms\": 5000}}","handlingStrategy":"try-catch","validationCode":"from hook_registry import HOOK_REGISTRY\n\ndef validate_spec(spec: dict):\n    entry = HOOK_REGISTRY.get(spec.get(\"action\"))\n    if entry is None:\n        raise ValueError(f\"unknown action {spec.get('action')!r}\")\n    return entry[\"params_model\"](**(spec.get(\"params\") or {}))  # raises early with field detail","typeGuard":"def has_required_params(spec: dict) -> bool:\n    entry = HOOK_REGISTRY.get(spec.get(\"action\"))\n    if not entry:\n        return False\n    required = entry[\"params_model\"].model_fields\n    params = spec.get(\"params\") or {}\n    return all(k in params for k, f in required.items() if f.is_required())","tryCatchPattern":"from hook_registry import build_declarative_hooks, HookValidationError\n\ntry:\n    hooks = build_declarative_hooks(specs)\nexcept HookValidationError as e:\n    # inner pydantic text after ': ' names the field and constraint\n    field_err = str(e).split(\": \", 1)[-1]\n    report_config_error(specs, field_err)","preventionTips":["Mirror each *_Params model's constraints (ge/le bounds, required fields) in the client schema.","Dry-run build_declarative_hooks on config load, not at crawl time.","Fuzz boundary values (0, max, max+1) in config tests."],"tags":["validation","pydantic","hooks","config"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}