Skip to content

fix(storage3): parse the real API error wire format - #1606

Open
grdsdev wants to merge 1 commit into
v3from
fix/v3-storage-api-error-parsing
Open

fix(storage3): parse the real API error wire format#1606
grdsdev wants to merge 1 commit into
v3from
fix/v3-storage-api-error-parsing

Conversation

@grdsdev

@grdsdev grdsdev commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1577. Neither fix from that PR is needed on v3 — pydantic's ValidationError already covers every malformed body shape, and exists() already branches on status rather than on an exception type. But verifying that surfaced a separate bug in the same code.

The bug

parse_api_error validated the response body against StorageApiError itself, whose fields are message / code / status. The API sends statusCode / error / message. No aliases are configured, so validation always failed and every API error fell into the fallback:

body before after
{"statusCode":"404","error":"not_found","message":"Object not found"} code='InternalError' status=400 code='not_found' status='404'
<html>502 Bad Gateway</html> code='InternalError' status=400 code='InternalError' status=502

A 404 was indistinguishable from a 500, and the real message was buried in Unable to parse error message: {raw json}. This affects all 9 parse_api_error call sites across file_api.py and vectors.py.

Two things suggest an oversight rather than a deliberate shape change:

  • VectorBucketErrorMessage sat in the same file describing the correct wire shape, imported nowhere.
  • The two existing error tests only assert pytest.raises(StorageApiError), never .code or .status — so the suite could not see it. Their mock body is already in the correct wire format.

Changes

  • Parse into StorageApiErrorMessage (the renamed, previously-dead model), then build StorageApiError from it. VectorBucketErrorMessage is kept as an alias — the vector endpoints return the same body.
  • Fallback reports response.status instead of a hardcoded 400.
  • Fallback decodes with errors="replace", so a non-utf-8 body cannot raise from the error path itself.
  • New tests/test_exceptions.py: 9 unit tests, no infra needed. All 9 fail on the current v3 code and pass with this change.
  • Tightened the two existing test_client_info_with_error tests to assert .message, .code and .status.

Verification

ruff check, ruff format and mypy clean; the 9 new unit tests pass. Docker is not available in my environment, so the integration suite was not run locally — CI covers it.

parse_api_error validated the response body against StorageApiError
itself (message/code/status), but the API sends statusCode/error/message.
Every API error therefore fell into the fallback and surfaced as
code=InternalError, status=400, with the raw JSON in the message -- a 404
was indistinguishable from a 500.

Parse into StorageApiErrorMessage, which already described the wire shape
but was unused. The fallback now reports the response status instead of a
hardcoded 400, and decodes with errors=replace so a non-utf-8 body cannot
raise from the error path.
@grdsdev
grdsdev requested review from a team and o-santi as code owners September 3, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant