tursodatabase/turso · error · ProtocolError

invalid cursor response

Error message

invalid cursor response: {e}

What it means

The first non-blank line of a cursor response body must be a JSON object describing the cursor. _parse_cursor_body() runs json.loads on it and, on failure, raises ProtocolError('invalid cursor response: ...') carrying the parser's message. In practice the body's first line is not protocol JSON at all - it is whatever actually answered the HTTP request.

Solutions

  1. Verify the URL points at the Turso/Hrana endpoint and uses the right scheme
  2. Check the auth token and custom headers are valid and being sent
  3. Inspect the first bytes of the raw body to identify what actually answered, then fix that layer
Defensive patterns

Strategy: try-catch

Validate before calling

def endpoint_is_json(url: str) -> bool:
    # Cheap preflight: an HTML answer here means wrong URL or intercepting proxy
    resp = http.get(url, timeout=5)
    ctype = resp.headers.get("content-type", "")
    return "json" in ctype or "octet-stream" in ctype

Try / catch

from turso_serverless.protocol import ProtocolError

try:
    result = session.execute_stmt(sql)
except ProtocolError as e:
    if "invalid cursor response" in str(e):
        raise RuntimeError(
            "Endpoint did not return the protocol; check URL, token, and proxies"
        ) from e
    raise

Prevention

When it happens

Trigger: An HTML or plain-text error page from a reverse proxy, WAF, or auth layer served where the protocol response was expected; a truncated first line; a wrong URL that returns a human-facing 404 page.

Common situations: Wrong endpoint URL (hits a website, not the database service); expired auth token producing an error body with a 200 status; corporate proxies injecting block or login pages.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

    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."""

    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

View on GitHub (pinned to c1e5928725)