unclecode/crawl4ai · error · ValueError

Hook '{hook_name}' must be a callable function, got {type(ho

Error message

Hook '{hook_name}' must be a callable function, got {type(hook_func)}

What it means

hooks_to_string (crawl4ai/utils.py:3726) raises ValueError when a value in the hooks dict passed to it is not callable. The function serializes hook functions into source-code strings for the Docker/remote API; before extracting source with inspect.getsource it asserts each hook is a function. Passing a string of code, a module, None, or an already-serialized source string triggers this.

Source

Thrown at crawl4ai/utils.py:3726

        Dictionary mapping hook point names to string representations of the functions.

    Example:
        >>> async def my_hook(page, context, **kwargs):
        ...     await page.set_viewport_size({"width": 1920, "height": 1080})
        ...     return page
        >>>
        >>> hooks_dict = {"on_page_context_created": my_hook}
        >>> api_hooks = hooks_to_string(hooks_dict)
        >>> # api_hooks is now ready to use with Docker API

    Raises:
        ValueError: If a hook is not callable or source cannot be extracted
    """
    result = {}

    for hook_name, hook_func in hooks.items():
        if not callable(hook_func):
            raise ValueError(f"Hook '{hook_name}' must be a callable function, got {type(hook_func)}")

        try:
            # Get the source code of the function
            source = inspect.getsource(hook_func)
            # Remove any leading indentation to get clean source
            source = textwrap.dedent(source)
            result[hook_name] = source
        except (OSError, TypeError) as e:
            raise ValueError(
                f"Cannot extract source code for hook '{hook_name}'. "
                f"Make sure the function is defined in a file (not interactively). Error: {e}"
            )

    return result

View on GitHub (pinned to 7e80152142)

Solutions

  1. Pass actual function objects, not source strings: define def my_hook(ctx): ... and use hooks_to_string({"on_page_context_created": my_hook}).
  2. If config arrives as JSON strings, exec/compile them into functions first (only for trusted input) or use the server's declarative hook specs instead.
  3. Add a unit assertion callable(h) for every hook before calling hooks_to_string to fail fast with your own message.

Example fix

# before
api_hooks = hooks_to_string({"on_page_context_created": "my_hook_source"})

# after
def my_hook(page, context, **kwargs):
    return None
api_hooks = hooks_to_string({"on_page_context_created": my_hook})
Defensive patterns

Strategy: type-guard

Type guard

from typing import Callable

def is_valid_hook_dict(hooks: dict) -> bool:
    return bool(hooks) and all(
        isinstance(k, str) and callable(v) and not isinstance(v, (str, bytes))
        for k, v in hooks.items()
    )

Try / catch

try:
    api_hooks = hooks_to_string(hooks)
except ValueError as e:
    if "must be a callable" in str(e):
        hooks = {k: v for k, v in hooks.items() if callable(v)}  # drop bad entries
    raise

Prevention

When it happens

Trigger: Calling hooks_to_string({"on_page_context_created": "def hook(ctx): ..."}) — passing source text instead of a function object; passing a class or builtin; a dict built from JSON config where values are strings by construction.

Common situations: Porting a JSON/YAML hook config to the Python API and forgetting to define actual functions; passing hook names instead of hook callables; refactoring that replaces functions with functools.partial of a non-callable.

Related errors


AI-assisted analysis of unclecode/crawl4ai@7e80152142 (2026-08-14). Data as JSON: /api/errors/253213ab6e8aa2fb. Report an issue: GitHub.