tursodatabase/turso · error · ProtocolError
invalid rowid in server response: {e}
Error message
invalid rowid in server response: {e} What it means
When decoding a single batch statement result, the SDK reads last_insert_rowid for INSERT/REPLACE statements and coerces it to int. If the server sends a rowid that isn't an integer (wrong type or non-numeric string), _decode_batch_statement_result raises ProtocolError. It protects callers from silently returning a bogus lastrowid.
Source
Thrown at serverless/python/turso_serverless/connection.py:424
"""Decode one statement result of a batch response (section 8.4),
or None when the statement did not complete."""
if not isinstance(step_result, dict):
return None
columns = [c.get("name") or "" for c in step_result.get("cols") or []]
rows = [tuple(decode_value(v) for v in row) for row in step_result.get("rows") or []]
if columns:
description = tuple((name, None, None, None, None, None, None) for name in columns)
rowcount = -1
else:
description = None
rowcount = step_result.get("affected_row_count") or 0
lastrowid = None
raw_rowid = step_result.get("last_insert_rowid")
if raw_rowid is not None and _is_insert_or_replace(sql):
try:
lastrowid = int(raw_rowid)
except (TypeError, ValueError) as e:
raise ProtocolError(f"invalid rowid in server response: {e}") from None
return BatchResult(
rows=rows,
description=description,
rowcount=rowcount,
lastrowid=lastrowid,
rows_read=step_result.get("rows_read"),
rows_written=step_result.get("rows_written"),
query_duration_ms=step_result.get("query_duration_ms"),
)
@staticmethod
def _raise_batch_step_error(
step_errors: list,
statement_count: int,
offset: int,
commit_index: int | None,
results: list,
) -> None:View on GitHub (pinned to c1e5928725)
Solutions
- Upgrade the SDK and server to aligned versions.
- Log the raw step_result for the failing statement (the ValueError in the message shows the bad value) and check what the server actually sent.
- If a proxy sits between client and server, bypass it to see whether the response changes.
- Only lastrowid for INSERT/REPLACE is parsed; as a workaround, RETURNING the rowid explicitly instead of relying on last_insert_rowid.
Example fix
# before
res = conn.batch([("INSERT INTO t DEFAULT VALUES", ())])[0]
rid = res.lastrowid # ProtocolError if server sent a bad rowid
# after
res = conn.batch([("INSERT INTO t DEFAULT VALUES RETURNING rowid", ())])[0]
rid = res.rows[0][0] # rowid comes back as a normal column value Defensive patterns
Strategy: try-catch
Type guard
def is_valid_rowid(raw: object) -> bool:
try:
int(raw)
return True
except (TypeError, ValueError):
return False Try / catch
try:
results = conn.batch(stmts)
rid = results[0].lastrowid
except ProtocolError as e:
if "invalid rowid" in str(e):
log.error("server sent non-integer rowid: %s", e)
rid = None # fall back to RETURNING-based lookup
else:
raise Prevention
- Prefer RETURNING rowid/id in INSERT statements over last_insert_rowid.
- Keep SDK and server versions aligned.
- Inspect raw step_result payloads when rowid decoding fails.
- Avoid intermediaries that re-serialize numeric JSON fields.
When it happens
Trigger: A batch whose INSERT/REPLACE step returns last_insert_rowid as a non-numeric value (string, null-shaped object, float-like string) — e.g. protocol drift, a proxy altering JSON, or a non-conformant server.
Common situations: Older/newer server emitting rowids in a different encoding, intermediaries re-serializing numbers as strings with unexpected content, or hitting an incompatible endpoint.
Related errors
- batch response does not have one result and one error per st
- batch response is missing the result for statement {i}
- expected batch result in pipeline response, got {response}
- invalid value in server response: {e}
- unsupported value type in server response: {typ!r}
AI-assisted analysis of tursodatabase/turso@c1e5928725 (2026-08-31).
Data as JSON: /api/errors/e623fd15dac8646f.
Report an issue: GitHub.