tursodatabase/turso · error · ProtocolError

invalid cursor entry: {e}

Error message

invalid cursor entry: {e}

What it means

ProtocolError raised by _parse_cursor_body() (session.py:71-75) when any NDJSON entry line after the cursor response line fails json.loads — the streamed step/row entries (section 7.2) are corrupt partway through the body. The stream is reset by Session.execute_stmt's ProtocolError handler, so a retry starts a fresh stream.

Source

Thrown at serverless/python/turso_serverless/session.py:75

_PROBE_STEP = 1


def _parse_cursor_body(raw: bytes) -> tuple[dict, list[dict]]:
    """Split a cursor response body (section 7.1) into the cursor response
    line and the decoded entries that follow it."""
    lines = [line for line in raw.decode("utf-8").splitlines() if line.strip()]
    if not lines:
        raise ProtocolError("cursor response body ended before the cursor response line")
    try:
        cursor_resp = json.loads(lines[0])
    except ValueError as e:
        raise ProtocolError(f"invalid cursor response: {e}") from None
    entries = []
    for line in lines[1:]:
        try:
            entries.append(json.loads(line))
        except ValueError as e:
            raise ProtocolError(f"invalid cursor entry: {e}") from None
    return cursor_resp, entries


class _CursorDecoder:
    """Folds streamed cursor entries (section 7.2) into a statement result,
    tracking the trailing autocommit probe step separately from the caller's
    step."""

    def __init__(self) -> None:
        self.result = StmtResult(columns=[], rows=[], affected_rows=0, last_insert_rowid=None)
        self.step_error: Optional[ServerError] = None
        self.fatal: Optional[ServerError] = None
        self.probe_executed = False
        self.probe_unreliable = False
        self._in_probe = False

    def feed(self, entry: dict) -> None:
        etype = entry.get("type")

View on GitHub (pinned to bad083fafb)

Solutions

  1. Retry the statement — the driver already reset the stream and the next call opens a new one
  2. Shrink responses (LIMIT/pagination, select fewer or smaller blob columns) if a proxy size threshold is implicated
  3. Raise intermediary read/idle timeouts and disable response rewriting on the path
Defensive patterns

Strategy: retry

Try / catch

from turso_serverless.protocol import ProtocolError

for attempt in range(3):
    try:
        rows = conn.execute(select_sql).fetchall()
        break
    except ProtocolError as e:
        if not str(e).startswith("invalid cursor entry") or attempt == 2:
            raise
        # truncated NDJSON: stream was reset; page smaller and retry
        select_sql = page_query(smaller_limit)

Prevention

When it happens

Trigger: A response truncated mid-body (last JSON line cut off) that still decoded as UTF-8; an intermediary appending or mangling lines; server bugs emitting malformed entries for specific value shapes.

Common situations: Proxies with response-size limits truncating large result sets; flaky links dropping tail bytes of chunked responses; rows containing very large blobs hitting an intermediary buffer limit.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/6ba3d0f2fca96b47. Report an issue: GitHub.