MemPalace/mempalace · error · DaemonError

daemon returned non-JSON response: {raw[:200]!r}

Error message

daemon returned non-JSON response: {raw[:200]!r}

What it means

DaemonError from DaemonClient.request (mempalace/daemon.py:1173): the daemon returned a 2xx response whose body is not valid JSON (json.loads raised JSONDecodeError). This covers empty 200s, truncated responses, and proxy/servlet HTML. It is wrapped so callers that only handle DaemonError get a uniform error type.

Source

Thrown at mempalace/daemon.py:1173

                raw = resp.read().decode("utf-8")
        except urlerror.HTTPError as exc:
            raw = exc.read().decode("utf-8", errors="replace")
            try:
                payload = json.loads(raw)
            except json.JSONDecodeError:
                payload = {"error": raw or str(exc)}
            raise DaemonError(str(payload.get("error", exc))) from exc
        except OSError as exc:
            raise DaemonError(str(exc)) from exc
        if not raw:
            return {}
        try:
            return json.loads(raw)
        except json.JSONDecodeError as exc:
            # A 2xx response with a non-JSON body (empty 200, truncated write,
            # proxy HTML) shouldn't surface as a bare JSONDecodeError to callers
            # that only know how to handle DaemonError.
            raise DaemonError(f"daemon returned non-JSON response: {raw[:200]!r}") from exc

    def health(self, *, timeout: float = 5.0) -> dict[str, Any]:
        return self.request("GET", "/health", timeout=timeout)

    def submit(
        self,
        kind: str,
        payload: dict[str, Any],
        *,
        dedupe_key: str | None = None,
        priority: int = 0,
    ) -> dict[str, Any]:
        return self.request(
            "POST",
            "/jobs",
            {"kind": kind, "payload": payload, "dedupe_key": dedupe_key, "priority": priority},
        )["job"]

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Check daemon liveness (client.health()) and the daemon log; restart if it crashed mid-response.
  2. Ensure no proxy/middleman touches 127.0.0.1 (the client already builds a no-proxy opener; keep env free of forced proxies).
  3. Update/align client and daemon versions so both sides agree on which routes return JSON.
  4. Retry the request once — truncated responses from a crashed daemon are not persistent once restarted.

Example fix

# before
try:
    result = client.request("POST", "/jobs", payload)
except json.JSONDecodeError:
    ...  # never caught; crashes caller

# after
try:
    result = client.request("POST", "/jobs", payload)
except DaemonError as exc:
    logger.warning("daemon call failed: %s", exc)  # includes non-JSON case
Defensive patterns

Strategy: retry

Try / catch

from mempalace.daemon import DaemonError

for attempt in range(2):
    try:
        return client.request("POST", "/jobs", payload)
    except DaemonError as exc:
        if "non-JSON" in str(exc) and attempt == 0:
            client = ensure_client(palace_path)  # daemon may have crashed; restart once
            continue
        raise

Prevention

When it happens

Trigger: A proxy or middlebox intercepting the loopback request and returning HTML; the daemon killed mid-response leaving a truncated body; an empty 200 from an edge case in the handler; anything between the client and server mutating the body.

Common situations: System-wide HTTP interception (corporate proxy env vars) despite the no-proxy opener covering most cases; daemon crash during a long response; OS-level security software tampering with loopback traffic; version skew where a route exists but returns an empty body.

Related errors


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