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
- Verify the URL points at the Turso/Hrana endpoint and uses the right scheme
- Check the auth token and custom headers are valid and being sent
- 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
- Verify the endpoint URL and scheme at startup with a minimal query
- Check token validity before long-running jobs
- Inspect the first bytes of failing bodies to identify HTML block pages early
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
- cursor response body ended before the cursor response line
- batch response does not have one result and one error per…
- batch response is missing the result for statement
- expected batch result in pipeline response, got
- infinite float values cannot be sent over the protocol
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] = NoneView on GitHub (pinned to c1e5928725)