tursodatabase/turso · error · ProtocolError
HTTP status {e.code}: {message}
Error message
HTTP status {e.code}: {message} What it means
ProtocolError raised by Session._post (session.py:193-208) for any non-2xx HTTP status. The driver reads the error body, tries to parse it as JSON, and embeds its 'error' or 'message' string after the status code. Before raising, it resets the stream (baton cleared, autocommit restored), which also rolls back any open transaction. This is the umbrella surface for auth failures, stale streams, rate limits, and server errors.
Source
Thrown at serverless/python/turso_serverless/session.py:207
req = urllib.request.Request(url, data=data, headers=self._headers(), method="POST")
try:
with urllib.request.urlopen(req) as resp:
return resp.read()
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
self._reset_stream()
message = None
try:
parsed = json.loads(raw)
if isinstance(parsed, dict):
for key in ("error", "message"):
if isinstance(parsed.get(key), str):
message = parsed[key]
break
except ValueError:
pass
if message is not None:
raise ProtocolError(f"HTTP status {e.code}: {message}") from None
raise ProtocolError(f"HTTP status {e.code}") from None
except urllib.error.URLError as e:
self._reset_stream()
raise ProtocolError(f"request to {url} failed: {e.reason}") from None
except (http.client.HTTPException, OSError) as e:
# Reading the body can fail after urlopen returned, e.g. with
# IncompleteRead on a truncated chunked response or a connection
# reset; URLError does not cover these, but they are equally
# fatal for the stream.
self._reset_stream()
raise ProtocolError(f"request to {url} failed: {e!r}") from None
def _update_stream(self, baton: Optional[str], base_url: Optional[str]) -> None:
self._baton = baton
if base_url:
self._base_url = normalize_url(base_url)
def execute_pipeline(self, requests: list[dict], track_autocommit: bool = True) -> list[dict]:View on GitHub (pinned to bad083fafb)
Solutions
- Branch on the code: 401/403 -> create a new token and reconnect; 404 -> fix the database URL; 429 -> back off (honor Retry-After); 5xx -> retry with jitter
- After any of these, keep using the same Connection — the stream was reset and the next statement opens a fresh one — or reconnect if the token itself was the problem
- Refresh tokens before expiry rather than after failure
- Add a startup probe (SELECT 1) to surface bad URLs and tokens at boot instead of on first user request
Defensive patterns
Strategy: retry
Validate before calling
def probe_connection(url: str, token: str) -> None:
"""Fail at startup, not on the first user request."""
conn = connect(url, auth_token=token)
conn.execute("SELECT 1").fetchall()
conn.close() Try / catch
import re, time
from turso_serverless.protocol import ProtocolError
_HTTP = re.compile(r"^HTTP status (\d{3})")
def run(conn_factory, sql, params=()):
for attempt in range(5):
try:
return conn_factory().execute(sql, params).fetchall()
except ProtocolError as e:
m = _HTTP.match(str(e))
if not m:
raise
code = int(m.group(1))
if code in (401, 403):
raise RuntimeError("auth failed: refresh token") from e
if code == 429 or code >= 500:
time.sleep(min(2 ** attempt, 30))
continue
raise Prevention
- Rotate/refresh auth tokens before expiry on a schedule, not on failure
- Store the full database URL (host + database path) in validated config
- Respect 429 Retry-After with backoff and jitter; never retry 4xx other than 429
- Add a boot-time SELECT 1 probe in long-running services
When it happens
Trigger: 401/403 with an invalid or expired auth token; 404 from a mistyped database URL; 4xx when a long-idle connection's baton/stream expired server-side; 429 rate limiting; 5xx server faults. All surface as 'HTTP status <code>: <server message>'.
Common situations: Expired Turso API tokens in long-running services; URLs copied without the database path or with the wrong region host; free-tier rate limits under burst load; connections idle for minutes to hours whose next statement hits a stale stream.
Related errors
- HTTP error! status: ${response.status}
- infinite float values cannot be sent over the protocol
- Unsupported value type: {type(value).__name__}
- invalid value in server response: {e}
- cursor response body ended before the cursor response line
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/30536b28528c46a9.
Report an issue: GitHub.