unclecode/crawl4ai · error · ValueError

control characters in value for header {name!r}

Error message

control characters in value for header {name!r}

What it means

SetHeadersParams' validator rejects header values containing \r, \n, or \x00. This is a classic CRLF/header-injection guard: a newline in a value would let a crafted config smuggle extra headers or split responses. Raised while validating a set_headers hook spec.

Source

Thrown at deploy/docker/hook_registry.py:78


class AddCookiesParams(BaseModel):
    cookies: List[_Cookie] = Field(..., min_length=1, max_length=_MAX_COOKIES)


class SetHeadersParams(BaseModel):
    headers: Dict[str, str]

    @field_validator("headers")
    @classmethod
    def _check(cls, v):
        if len(v) > _MAX_HEADERS:
            raise ValueError(f"too many headers (max {_MAX_HEADERS})")
        for name, value in v.items():
            if not _HEADER_NAME_RE.match(name):
                raise ValueError(f"invalid header name {name!r}")
            if any(c in value for c in "\r\n\x00"):
                raise ValueError(f"control characters in value for header {name!r}")
        return v


class ScrollToBottomParams(BaseModel):
    max_steps: int = Field(10, ge=1, le=_MAX_SCROLL_STEPS)
    delay_ms: int = Field(500, ge=0, le=_MAX_SCROLL_DELAY_MS)


class WaitForTimeoutParams(BaseModel):
    timeout_ms: int = Field(..., ge=0, le=_MAX_WAIT_MS)


# ───────────────────────── server-authored hook factories ─────────────────────────
def _factory_block_resources(p: BlockResourcesParams):
    types = set(p.resource_types)

    async def hook(page, **kwargs):
        context = kwargs.get("context")

View on GitHub (pinned to 7e80152142)

Solutions

  1. Strip/replace newlines: value.strip().replace('\r', '').replace('\n', '').
  2. Encode multi-value headers per RFC: 'Accept-Encoding: gzip, deflate, br' (comma-separated, single line).
  3. If a NUL appears, the value is binary — pass it base64-encoded or not at all.

Example fix

# before
{"headers": {"Cookie": "a=1\nb=2\n"}}

# after
{"headers": {"Cookie": "a=1; b=2"}}
Defensive patterns

Strategy: validation

Validate before calling

def clean_header_value(v: str) -> str:
    return v.replace("\r", "").replace("\n", "").replace("\x00", "").strip()

def safe_headers(headers: dict) -> dict:
    return {k: clean_header_value(v) for k, v in headers.items()}

Type guard

def is_crlf_free(value) -> bool:
    return isinstance(value, str) and not any(c in value for c in "\r\n\x00")

Try / catch

try:
    hooks = build_declarative_hooks(specs)
except HookValidationError as e:
    if "control characters" in str(e):
        specs = sanitize_header_values(specs)  # strip \r\n\x00, rebuild
    else:
        raise

Prevention

When it happens

Trigger: Passing a multi-line value (e.g. a pasted cookie block or a value with a trailing \n), or embedding CR/LF/NUL in any set_headers value.

Common situations: Pasting a value from a file that keeps a trailing newline; building values with '\n'.join(...) for multi-value headers instead of the comma convention; JSON configs where an escaped \n slips into a string.

Related errors


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