MemPalace/mempalace · error · ValueError

content must be a non-empty string

Error message

content must be a non-empty string

What it means

put_artifact requires `content` to be a non-empty str. Empty artifacts are meaningless (v1 stores exact UTF-8 text with a sha256 digest, and an empty payload has no content to verify), and None is not treated as ''. Binary or structured values are also rejected here since content must be text.

Source

Thrown at mempalace/logstream.py:526

        kind: str,
        content: str,
        created_by: str,
        metadata: dict = None,
    ) -> dict:
        """Store exact artifact content (v1: UTF-8 text only).

        Returns the artifact record without echoing ``content`` back —
        callers already hold the content; readers use :meth:`get_artifact`.
        For ``kind=patch``, a ``warnings`` list is included when the diff
        looks unappliable (missing trailing newline, CRLF endings); the
        content itself is still stored verbatim.
        """
        if not isinstance(kind, str) or kind not in ARTIFACT_KINDS:
            allowed = ", ".join(sorted(ARTIFACT_KINDS))
            raise ValueError(f"kind={kind!r} is not one of: {allowed}")
        created_by = _sanitize_routing(created_by, "created_by")
        if not isinstance(content, str) or not content:
            raise ValueError("content must be a non-empty string")
        if "\x00" in content:
            raise ValueError("content contains null bytes")
        content = strip_lone_surrogates(content)
        raw = content.encode("utf-8")
        if len(raw) > self.max_artifact_bytes:
            raise ValueError(
                f"content is {len(raw)} bytes; maximum is {self.max_artifact_bytes} bytes"
            )
        metadata_json = _sanitize_metadata(metadata)

        artifact_id = _new_id("art")
        created_at = _utc_now_iso()
        digest = sha256(raw).hexdigest()

        with self._lock:
            conn = self._conn()
            with conn:
                conn.execute(

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Skip the put_artifact call when content is falsy: if not content: return.
  2. If the payload is genuinely empty by design, store a sentinel note ('(empty)') or record the fact in the event body instead.
  3. Ensure text, not bytes: content=raw.decode('utf-8').

Example fix

// before
art = ls.put_artifact(kind="file", content=open(p).read(), ...)  # empty file
// after
content = open(p).read()
if content:
    art = ls.put_artifact(kind="file", content=content, ...)
else:
    art = None  # nothing to store
Defensive patterns

Strategy: validation

Validate before calling

def storable_content(content) -> bool:
    return isinstance(content, str) and bool(content)

if not storable_content(content):
    return None  # nothing to store; skip put_artifact entirely

Type guard

def is_storable_text(content) -> bool:
    return isinstance(content, str) and len(content) > 0

Try / catch

try:
    art = ls.put_artifact(kind=kind, content=content, ...)
except ValueError as e:
    if "non-empty string" in str(e):
        return None  # treat empty payload as no-op
    raise

Prevention

When it happens

Trigger: put_artifact(kind='file', content='') after reading an empty file; content=None when a template render produced nothing; content=b'bytes'; content=[] from a failed subprocess capture.

Common situations: Reading a file that is empty or was deleted mid-run; a diff generator returning '' on no changes; subprocess.check_output errors swallowed and passed through as None.

Related errors


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