{"record":{"id":"a97be9527a76f243","repo":"tursodatabase/turso","slug":"cursor-response-body-ended-before-the-cursor-respo","errorCode":null,"errorMessage":"cursor response body ended before the cursor response line","messagePattern":"cursor response body ended before the cursor response line","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"serverless/python/turso_serverless/session.py","lineNumber":65,"sourceCode":"class StmtResult:\n    \"\"\"The decoded output of one executed statement.\"\"\"\n\n    columns: list[str]\n    rows: list[tuple]\n    affected_rows: int\n    last_insert_rowid: Optional[int]\n\n\n# Index of the autocommit probe step appended to every cursor batch.\n_PROBE_STEP = 1\n\n\ndef _parse_cursor_body(raw: bytes) -> tuple[dict, list[dict]]:\n    \"\"\"Split a cursor response body (section 7.1) into the cursor response\n    line and the decoded entries that follow it.\"\"\"\n    lines = [line for line in raw.decode(\"utf-8\").splitlines() if line.strip()]\n    if not lines:\n        raise ProtocolError(\"cursor response body ended before the cursor response line\")\n    try:\n        cursor_resp = json.loads(lines[0])\n    except ValueError as e:\n        raise ProtocolError(f\"invalid cursor response: {e}\") from None\n    entries = []\n    for line in lines[1:]:\n        try:\n            entries.append(json.loads(line))\n        except ValueError as e:\n            raise ProtocolError(f\"invalid cursor entry: {e}\") from None\n    return cursor_resp, entries\n\n\nclass _CursorDecoder:\n    \"\"\"Folds streamed cursor entries (section 7.2) into a statement result,\n    tracking the trailing autocommit probe step separately from the caller's\n    step.\"\"\"\n","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/serverless/python/turso_serverless/session.py#L47-L83","documentation":"ProtocolError raised by _parse_cursor_body() (session.py:60-65) when the /v3/cursor response body contains no non-empty lines — the stream ended before the required cursor response line (protocol section 7.1). Session.execute_stmt catches it, resets the stream (baton cleared, autocommit restored), and re-raises, so the connection itself survives; the next statement opens a fresh stream.","triggerScenarios":"The server returns HTTP 200 with an empty or whitespace-only body for a cursor batch; an intermediary (LB, API gateway) substitutes an empty body; a server bug drops the body while keeping the status.","commonSituations":"Misconfigured reverse proxy or load balancer in front of the database endpoint; serverless platforms stripping streaming responses; transient server faults during deploys.","solutions":["Point the client directly at the database HTTP endpoint and retry — the driver reset the stream, so the next statement starts clean","Fix or remove the proxy that produced the empty body","If it reproduces against the real endpoint, upgrade client/server and report with the request body that triggered it"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from turso_serverless.protocol import ProtocolError\n\nfor attempt in range(2):\n    try:\n        rows = conn.execute(sql).fetchall()\n        break\n    except ProtocolError as e:\n        if \"ended before the cursor response line\" not in str(e) or attempt:\n            raise\n        # driver reset the stream; next statement starts a new one","preventionTips":["Health-check the real endpoint (SELECT 1) in CI so empty-body proxies are caught before deploy","Do not put rewriting proxies or caches in front of the database URL","Keep one retry-with-fresh-stream wrapper for transient response-shape failures"],"tags":["python","protocol","cursor","empty-body"],"backgroundTag":"empty-response-body","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}