langflow-ai/langflow · warning · HTTPException

Metadata key {key!r} is invalid: must be 1-{KB_METADATA_MAX_

Error message

Metadata key {key!r} is invalid: must be 1-{KB_METADATA_MAX_KEY_LENGTH} lowercase alphanumeric or underscore characters.

What it means

Every metadata key must be 1-32 characters (KB_METADATA_MAX_KEY_LENGTH) and consist only of lowercase letters, digits, and underscores — the exact charset in _KEY_ALLOWED_CHARS. The key is also rejected if it is not a string at all. Uppercase letters, hyphens, spaces, dots, and unicode all fail with this 422 message, which interpolates the offending key via repr().

Source

Thrown at src/backend/base/langflow/api/utils/kb_metadata.py:85

    """Enforce the user-metadata contract on a decoded dict.

    Returns the same dict (a shallow copy is *not* made — callers may mutate
    safely once validation passes). Raises :class:`HTTPException` with a 422
    status on any violation so FastAPI surfaces an inline error.
    """
    if not isinstance(metadata, dict):
        msg = "Metadata must be a JSON object."
        raise HTTPException(status_code=422, detail=msg)
    if len(metadata) > KB_METADATA_MAX_KEYS:
        msg = f"Metadata exceeds the {KB_METADATA_MAX_KEYS} key limit."
        raise HTTPException(status_code=422, detail=msg)
    for key, value in metadata.items():
        if not isinstance(key, str) or not _is_valid_key(key):
            msg = (
                f"Metadata key {key!r} is invalid: must be 1-{KB_METADATA_MAX_KEY_LENGTH} "
                "lowercase alphanumeric or underscore characters."
            )
            raise HTTPException(status_code=422, detail=msg)
        if key in KB_METADATA_RESERVED_KEYS:
            msg = f"Metadata key '{key}' is reserved for ingestion-internal use."
            raise HTTPException(status_code=422, detail=msg)
        _validate_value(key, value)
    return metadata


def parse_user_metadata(raw: str | None) -> dict[str, Any]:
    """Decode + validate the ``metadata`` form field. Empty/None → ``{}``."""
    if not raw:
        return {}
    try:
        decoded = json.loads(raw)
    except json.JSONDecodeError as exc:
        msg = f"Metadata is not valid JSON: {exc.msg}"
        raise HTTPException(status_code=422, detail=msg) from exc
    return validate_user_metadata(decoded)

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Normalize keys before submission: lower(), replace non-alphanumerics with '_', trim to 32 chars, collapse repeats.
  2. Adopt a fixed snake_case vocabulary for metadata keys in your ingestion pipeline.
  3. Watch out for a normalization collision ('Doc-Type' and 'doc type' both -> 'doc_type') — dedupe after normalizing.
  4. Add a regex guard on the client: ^[a-z0-9_]{1,32}$ per key.

Example fix

# before
metadata = {"Doc-Type": "pdf", "file.name": "a.pdf"}  # 422

# after
import re
def norm_key(k: str) -> str:
    k2 = re.sub(r'[^a-z0-9_]', '_', k.lower())[:32]
    return k2
metadata = {norm_key(k): v for k, v in raw.items()}
Defensive patterns

Strategy: validation

Validate before calling

import re
from langflow.utils.kb_constants import KB_METADATA_MAX_KEY_LENGTH as MAXKL

def normalize_keys(meta: dict) -> dict:
    out = {}
    for k, v in meta.items():
        nk = re.sub(r'[^a-z0-9_]', '_', k.lower())[:MAXKL].strip('_') or 'key'
        while nk in out:
            nk = (nk[:MAXKL-2] + f'_{len(out)}')
        out[nk] = v
    return out

Type guard

KEY_RE = re.compile(r'^[a-z0-9_]{1,32}$')

def has_valid_keys(meta) -> bool:
    return all(isinstance(k, str) and KEY_RE.match(k) for k in meta)

Try / catch

try:
    validate_user_metadata(meta)
except HTTPException as e:
    if e.status_code == 422 and 'is invalid: must be' in e.detail:
        meta = normalize_keys(meta); validate_user_metadata(meta)
    else:
        raise

Prevention

When it happens

Trigger: metadata JSON with keys like 'Source-Type', 'file.name', 'category 1', 'ключ', or a 40-char key. Also a non-string JSON key is impossible via json.loads (JSON keys are always strings), so in practice the length/charset predicate is what fails. Applies identically to per-file inner dicts.

Common situations: Copying metadata field names from HTTP headers, file-stat structs, or camelCase ORM fields; LLM-generated metadata keys in natural language ('Document Type'); i18n data with non-ASCII keys; namespaced keys like 'x.custom.label'.

Related errors


AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14). Data as JSON: /api/errors/9d36a3af356eb889. Report an issue: GitHub.