Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore(API): validate JSON input for APIError.__init__()
directly passing `r.json()` into APIError.__init__() is incorrect, as
it expects a `Dict[str, str]`. instead, it should first validate that
the json object is in fact the correct schema, by using a pydantic model
  • Loading branch information
o-santi committed May 16, 2025
commit 0bbb2f043de5c3e221a596b20dadd010b5177ebc
12 changes: 5 additions & 7 deletions postgrest/_async/request_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
pre_update,
pre_upsert,
)
from ..exceptions import APIError, generate_default_error_message
from ..exceptions import APIError, APIErrorFromJSON, generate_default_error_message
from ..types import ReturnMethod
from ..utils import AsyncClient, get_origin_and_cast

Expand Down Expand Up @@ -75,10 +75,9 @@ async def execute(self) -> APIResponse[_ReturnT]:
return body
return APIResponse[_ReturnT].from_http_request_response(r)
else:
raise APIError(r.json())
json_obj = APIErrorFromJSON.model_validate_json(r.content)
raise APIError(dict(json_obj))
except ValidationError as e:
raise APIError(r.json()) from e
except JSONDecodeError:
raise APIError(generate_default_error_message(r))


Expand Down Expand Up @@ -124,10 +123,9 @@ async def execute(self) -> SingleAPIResponse[_ReturnT]:
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
return SingleAPIResponse[_ReturnT].from_http_request_response(r)
else:
raise APIError(r.json())
json_obj = APIErrorFromJSON.model_validate_json(r.content)
raise APIError(dict(json_obj))
except ValidationError as e:
raise APIError(r.json()) from e
except JSONDecodeError:
raise APIError(generate_default_error_message(r))


Expand Down
15 changes: 15 additions & 0 deletions postgrest/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
from typing import Dict, Optional
from pydantic import BaseModel


class APIErrorFromJSON(BaseModel):
"""
A pydantic object to validate an error info object
from a json string.
"""
message: Optional[str]
"""The error message."""
code: Optional[str]
"""The error code."""
hint: Optional[str]
"""The error hint."""
details: Optional[str]
"""The error details."""

class APIError(Exception):
"""
Base exception for all API errors.
Expand Down
27 changes: 25 additions & 2 deletions tests/_async/test_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest.mock import patch

import pytest
from httpx import BasicAuth, Headers
from httpx import BasicAuth, Headers, Response, Request

from postgrest import AsyncPostgrestClient
from postgrest.exceptions import APIError
Expand Down Expand Up @@ -107,7 +107,6 @@ async def test_response_status_code_outside_ok(postgrest_client: AsyncPostgrestC
)
assert exc_response["errors"][0].get("code") == 400


@pytest.mark.asyncio
async def test_response_maybe_single(postgrest_client: AsyncPostgrestClient):
with patch(
Expand All @@ -127,3 +126,27 @@ async def test_response_maybe_single(postgrest_client: AsyncPostgrestClient):
exc_response = exc_info.value.json()
assert isinstance(exc_response.get("message"), str)
assert "code" in exc_response and int(exc_response["code"]) == 204

# https://github.com/supabase/postgrest-py/issues/595
@pytest.mark.asyncio
async def test_response_client_invalid_response_but_valid_json(postgrest_client: AsyncPostgrestClient):
with patch(
"httpx._client.AsyncClient.request",
return_value=Response(
status_code=502,
text='"gateway error: Error: Network connection lost."', # quotes makes this text a valid non-dict JSON object
request=Request(method="GET", url="http://example.com")
)
):
client = (
postgrest_client.from_("test").select("a", "b").eq("c", "d").single()
)
assert "Accept" in client.headers
assert client.headers.get("Accept") == "application/vnd.pgrst.object+json"
with pytest.raises(APIError) as exc_info:
await client.execute()
assert isinstance(exc_info, pytest.ExceptionInfo)
exc_response = exc_info.value.json()
assert isinstance(exc_response.get("message"), str)
assert exc_response.get("message") == "JSON could not be generated"
assert "code" in exc_response and int(exc_response["code"]) == 502
Loading