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
- If the payload is structured, serialize it yourself: body=json.dumps(payload, ensure_ascii=False).
- If it came from a file, open in text mode or decode explicitly: body=data.decode('utf-8').
- 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
- Serialize structured payloads with json.dumps at the call site, not inside a catch-all handler.
- Open files in text mode ('r') when their contents become bodies.
- Type-annotate producer functions (body: str) so mypy catches dict/bytes flows.
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
- {field_name} is {size} bytes; maximum is {max_bytes} bytes
- metadata must be an object
- artifact_ids must be a list of artifact id strings
- {field_name} must be a non-empty string
- {field_name} exceeds maximum length of {_MAX_ROUTING_LENGTH}
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/f8893d469a746392.
Report an issue: GitHub.