tursodatabase/turso · error · ProtocolError

cursor response body ended before the cursor response line

Error message

cursor response body ended before the cursor response line

What it means

_parse_cursor_body() splits a streamed cursor response (section 7.1) into a first JSON line plus the entry lines that follow. It first strips blank lines; if nothing remains, it raises ProtocolError('cursor response body ended before the cursor response line'). The HTTP layer returned a body, but that body carried no payload at all - the stream ended before any cursor response line appeared.

Solutions

  1. Retry the request with backoff - an empty body is often transient
  2. Bypass or reconfigure proxies that truncate streaming responses
  3. If it reproduces consistently, capture the raw response (status, headers, body length) and report it
Defensive patterns

Strategy: retry

Try / catch

from turso_serverless.protocol import ProtocolError

for attempt in range(3):
    try:
        result = session.execute_stmt(sql)
        break
    except ProtocolError as e:
        if "ended before the cursor response line" not in str(e) or attempt == 2:
            raise
        time.sleep(0.5 * (2 ** attempt))  # empty body: usually transient

Prevention

When it happens

Trigger: Server returns an empty 200 body; a proxy or flaky network truncates the streamed response to nothing; code that reads the response body twice and gets an empty second read.

Common situations: Load balancers or CDNs stripping streamed bodies; mobile networks dropping mid-response; server bugs on error paths that still return 200 with no content.

Related errors


AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-20). Data as JSON: /api/errors/a97be9527a76f243. Report an issue: GitHub.

Appendix: source

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

class StmtResult:
    """The decoded output of one executed statement."""

    columns: list[str]
    rows: list[tuple]
    affected_rows: int
    last_insert_rowid: Optional[int]


# Index of the autocommit probe step appended to every cursor batch.
_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."""

View on GitHub (pinned to c1e5928725)