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

  1. 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
  2. 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
  3. Refresh tokens before expiry rather than after failure
  4. 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

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


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/30536b28528c46a9. Report an issue: GitHub.