MemPalace/mempalace · error · ValueError

{field_name} must be a string

Error message

{field_name} must be a string

What it means

_sanitize_body requires the event `body` (and any field validated through it) to be a Python str. None is accepted and normalized to '', but any other type — int, dict, list, bytes — raises this error before storage. The body is a verbatim UTF-8 text payload, so binary or structured values are rejected at the boundary.

Source

Thrown at mempalace/logstream.py:146

        )
    return value


def _sanitize_status(value) -> Optional[str]:
    if value is None or value == "":
        return None
    if not isinstance(value, str) or value not in EVENT_STATUSES:
        allowed = ", ".join(sorted(EVENT_STATUSES))
        raise ValueError(f"status={value!r} is not one of: {allowed}")
    return value


def _sanitize_body(value, max_bytes: int, field_name: str = "body") -> str:
    """Validate verbatim payload text. Empty is allowed; ``None`` becomes ''."""
    if value is None:
        return ""
    if not isinstance(value, str):
        raise ValueError(f"{field_name} must be a string")
    if "\x00" in value:
        raise ValueError(f"{field_name} contains null bytes")
    value = strip_lone_surrogates(value)
    size = len(value.encode("utf-8"))
    if size > max_bytes:
        raise ValueError(f"{field_name} is {size} bytes; maximum is {max_bytes} bytes")
    return value


def _sanitize_metadata(value) -> str:
    """Validate optional metadata dict and return its canonical JSON text."""
    if value is None:
        return "{}"
    if not isinstance(value, dict):
        raise ValueError("metadata must be an object")
    try:
        encoded = json.dumps(value, ensure_ascii=False, sort_keys=True)
    except (TypeError, ValueError) as exc:

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. If the payload is structured, serialize it yourself: body=json.dumps(payload, ensure_ascii=False).
  2. If it came from a file, open in text mode or decode explicitly: body=data.decode('utf-8').
  3. For an empty body, pass body=None or body='' rather than 0 or {}.

Example fix

// before
evt = ls.append_event(type="task.request", body={"task": "fix ranking"}, ...)
// after
import json
evt = ls.append_event(type="task.request", body=json.dumps({"task": "fix ranking"}, ensure_ascii=False), ...)
Defensive patterns

Strategy: type-guard

Validate before calling

def coerce_body(body):
    if body is None:
        return ""
    if isinstance(body, bytes):
        return body.decode("utf-8")
    if isinstance(body, (dict, list)):
        return json.dumps(body, ensure_ascii=False)
    if not isinstance(body, str):
        raise TypeError(f"body must be text, got {type(body).__name__}")
    return body

body = coerce_body(raw_payload)

Type guard

def is_valid_body(b) -> bool:
    return b is None or isinstance(b, str)

Try / catch

try:
    evt = ls.append_event(..., body=body)
except ValueError as e:
    if "must be a string" in str(e) and not isinstance(body, str):
        evt = ls.append_event(..., body=json.dumps(body, ensure_ascii=False))
    else:
        raise

Prevention

When it happens

Trigger: append_event(body={'text': 'fix ranking'}) (dict); body=12345; body=b'raw bytes'; ack_event(body=['line1','line2']). Note body='' and body=None are both fine.

Common situations: Callers serializing structured payloads and forgetting json.dumps(); MCP tool handlers passing through JSON objects unmodified; code that previously wrote bytes from a file read in 'rb' mode.

Related errors


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/f8893d469a746392. Report an issue: GitHub.