Skip to content

Commit

Permalink
Nice custom error wrapper for pydanic issues. Adds a "message" field …
Browse files Browse the repository at this point in the history
…with user readable errors, while keeping the source_errors as well
  • Loading branch information
scosman committed Oct 7, 2024
1 parent c813dad commit a66e6ca
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 3 deletions.
8 changes: 5 additions & 3 deletions app/web_ui/src/lib/utils/form_container.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,11 @@
</div>
{/if}
{#if custom_error_message}
<div class="text-sm text-center text-error">
{custom_error_message}
</div>
{#each custom_error_message.split("\n") as error_line}
<div class="text-sm text-center text-error">
{error_line}
</div>
{/each}
{/if}
<button
type="submit"
Expand Down
24 changes: 24 additions & 0 deletions libs/studio/kiln_studio/custom_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse


def connect_custom_errors(app: FastAPI):
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
):
# Write user friendly error messages
error_messages = []
for error in exc.errors():
field = error["loc"][-1] # Get the field name
message = error["msg"]
error_messages.append(f"{field.capitalize()}: {message}")

return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"message": ".\n".join(error_messages),
"source_errors": exc.errors(),
},
)
2 changes: 2 additions & 0 deletions libs/studio/kiln_studio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from .custom_errors import connect_custom_errors
from .project_management import connect_project_management
from .provider_management import connect_provider_management
from .settings import connect_settings
Expand All @@ -30,6 +31,7 @@ def ping():
connect_provider_management(app)
connect_settings(app)
connect_webhost(app)
connect_custom_errors(app)

return app

Expand Down
74 changes: 74 additions & 0 deletions libs/studio/kiln_studio/test_custom_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

from libs.studio.kiln_studio.custom_errors import connect_custom_errors


@pytest.fixture
def app():
app = FastAPI()
connect_custom_errors(app)

class Item(BaseModel):
name: str = Field(..., min_length=3)
price: float = Field(..., gt=0)

@app.post("/items")
async def create_item(item: Item):
return item

return app


@pytest.fixture
def client(app):
return TestClient(app)


def test_validation_error_single_field(client):
response = client.post("/items", json={"name": "ab", "price": 10})
assert response.status_code == 422
assert response.json() == {
"message": "Name: String should have at least 3 characters",
"source_errors": [
{
"type": "string_too_short",
"loc": ["body", "name"],
"msg": "String should have at least 3 characters",
"input": "ab",
"ctx": {"min_length": 3},
}
],
}


def test_validation_error_multiple_fields(client):
response = client.post("/items", json={"name": "ab", "price": -5})
assert response.status_code == 422
assert response.json() == {
"message": "Name: String should have at least 3 characters.\nPrice: Input should be greater than 0",
"source_errors": [
{
"type": "string_too_short",
"loc": ["body", "name"],
"msg": "String should have at least 3 characters",
"input": "ab",
"ctx": {"min_length": 3},
},
{
"type": "greater_than",
"loc": ["body", "price"],
"msg": "Input should be greater than 0",
"input": -5,
"ctx": {"gt": 0},
},
],
}


def test_valid_input(client):
response = client.post("/items", json={"name": "abc", "price": 10})
assert response.status_code == 200
assert response.json() == {"name": "abc", "price": 10}

0 comments on commit a66e6ca

Please sign in to comment.