{"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/c1e59287258d99b309e362a63f48822256e2f65f/serverless/python/turso_serverless/session.py#L47-L83","documentation":"_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.","triggerScenarios":"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.","commonSituations":"Load balancers or CDNs stripping streamed bodies; mobile networks dropping mid-response; server bugs on error paths that still return 200 with no content.","solutions":["Retry the request with backoff - an empty body is often transient","Bypass or reconfigure proxies that truncate streaming responses","If it reproduces consistently, capture the raw response (status, headers, body length) and report it"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"from turso_serverless.protocol import ProtocolError\n\nfor attempt in range(3):\n    try:\n        result = session.execute_stmt(sql)\n        break\n    except ProtocolError as e:\n        if \"ended before the cursor response line\" not in str(e) or attempt == 2:\n            raise\n        time.sleep(0.5 * (2 ** attempt))  # empty body: usually transient","preventionTips":["Wrap streaming calls in bounded retry with jitter","Health-check any proxy in front of the database endpoint","Log raw body length on decode failures to distinguish empty from truncated"],"tags":["python","protocol","http","empty-response","streaming"],"backgroundTag":"empty-response-body","analyzedSha":"c1e59287258d99b309e362a63f48822256e2f65f","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}