Exceptions¶
Custom exception classes and error handling.
Exception Hierarchy¶
StrideException
├── ValidationException
│ ├── FieldValidationError
│ ├── UniqueConstraintError
│ └── MultipleValidationErrors
├── DatabaseException
│ ├── DoesNotExist
│ ├── MultipleObjectsReturned
│ ├── IntegrityError
│ └── ConnectionError
├── AuthException
│ ├── AuthenticationFailed
│ ├── InvalidCredentials
│ ├── InvalidToken
│ ├── TokenExpired
│ ├── PermissionDenied
│ ├── UserInactive
│ └── UserNotFound
└── BusinessException
├── ResourceLocked
├── PreconditionFailed
├── OperationNotAllowed
└── QuotaExceeded
HTTPException
├── BadRequest (400)
├── Unauthorized (401)
├── Forbidden (403)
├── NotFound (404)
├── MethodNotAllowed (405)
├── Conflict (409)
├── UnprocessableEntity (422)
├── TooManyRequests (429)
├── InternalServerError (500)
└── ServiceUnavailable (503)
Usage¶
Validation Errors¶
from strider.exceptions import ValidationException, FieldValidationError
# Single field error
raise FieldValidationError(
message="Invalid email format",
field="email",
code="invalid_email"
)
# Multiple errors
from strider.exceptions import MultipleValidationErrors
errors = [
FieldValidationError("Email required", field="email"),
FieldValidationError("Password too short", field="password"),
]
raise MultipleValidationErrors(errors)
# Unique constraint
from strider.exceptions import UniqueConstraintError
raise UniqueConstraintError(
message="Email already exists",
field="email"
)
Database Errors¶
from strider.exceptions import DoesNotExist, MultipleObjectsReturned
# Not found
raise DoesNotExist(
message="User not found",
model="User",
lookup={"id": 1}
)
# Multiple results
raise MultipleObjectsReturned(
message="Multiple users found",
model="User",
count=3
)
Auth Errors¶
from strider.exceptions import (
AuthenticationFailed,
InvalidCredentials,
InvalidToken,
TokenExpired,
PermissionDenied,
)
raise AuthenticationFailed("Authentication required")
raise InvalidCredentials("Wrong email or password")
raise InvalidToken("Token is invalid")
raise TokenExpired("Token has expired")
raise PermissionDenied(
message="Cannot edit this resource",
permission="posts.edit",
resource="Post"
)
HTTP Errors¶
from strider.exceptions import (
BadRequest,
Unauthorized,
Forbidden,
NotFound,
Conflict,
TooManyRequests,
)
# 400 Bad Request
raise BadRequest("Invalid request")
raise BadRequest.with_field("email", "Invalid format")
# 401 Unauthorized
raise Unauthorized("Login required")
# 403 Forbidden
raise Forbidden("Access denied")
raise Forbidden.for_resource("Post", "delete")
# 404 Not Found
raise NotFound("Resource not found")
raise NotFound.for_model("User", id=1)
# 409 Conflict
raise Conflict("Resource already exists")
raise Conflict.duplicate("email", "user@example.com")
# 429 Too Many Requests
raise TooManyRequests(
message="Rate limit exceeded",
retry_after=60
)
Business Errors¶
from strider.exceptions import (
ResourceLocked,
PreconditionFailed,
OperationNotAllowed,
QuotaExceeded,
)
raise ResourceLocked("Document is being edited")
raise PreconditionFailed("Version mismatch")
raise OperationNotAllowed("Cannot delete active subscription")
raise QuotaExceeded("Storage limit reached")
Error Response Format¶
All exceptions return consistent JSON:
{
"detail": "Error message",
"code": "error_code"
}
Validation Errors¶
{
"detail": "Validation error",
"code": "validation_error",
"errors": [
{
"message": "Invalid email format",
"code": "invalid_email",
"field": "email"
}
]
}
Unique Constraint¶
{
"detail": "Email already exists",
"code": "unique_constraint",
"field": "email",
"value": "user@example.com"
}
Not Found¶
{
"detail": "User not found",
"code": "does_not_exist"
}
Integridade na base (SQLAlchemy IntegrityError)¶
O StrideApp converte erros do motor (PostgreSQL/asyncpg, SQLite, etc.) para uma resposta estável:
não envia SQL, stack nem mensagem bruta do driver. O detalhe técnico fica só nos logs do servidor.
Códigos possíveis:
code |
Significado típico | HTTP |
|---|---|---|
foreign_key_violation |
FK: registo referenciado não existe | 400 |
unique_constraint |
Valor duplicado (único) | 409 |
required_field |
NOT NULL violado | 422 |
integrity_error |
Outro (genérico) | 400 |
Exemplo (FK — texto em português no handler):
{
"detail": "Referência inválida: o registo associado não existe ou não pode ser usado.",
"code": "foreign_key_violation",
"field": "author_id",
"hint": "Confirme que o identificador enviado existe na entidade referenciada (ex.: utilizador / recurso pai)."
}
Schema Pydantic para documentar no OpenAPI (respostas 400/409/422):
from strider import DatabaseIntegrityResponse
Exception Handlers¶
StrideApp auto-registers handlers for:
- Pydantic
ValidationError→ 422 - Core
ValidationError→ 422 MultipleValidationErrors→ 422UniqueValidationError→ 409- SQLAlchemy
IntegrityError→ 400/409/422 (resposta sanitizada; ver secção acima) - SQLAlchemy
DataError→ 422 - SQLAlchemy
OperationalError→ 503 - Generic
Exception→ 500
Custom Exception¶
from strider.exceptions import StrideException
class PaymentFailedException(StrideException):
"""Payment processing failed."""
def __init__(
self,
message: str = "Payment failed",
code: str = "payment_failed",
transaction_id: str | None = None,
):
super().__init__(message=message, code=code)
self.transaction_id = transaction_id
def to_dict(self) -> dict:
data = super().to_dict()
if self.transaction_id:
data["transaction_id"] = self.transaction_id
return data
Custom Handler¶
from fastapi import Request
from fastapi.responses import JSONResponse
from strider import StrideApp
app = StrideApp()
@app.app.exception_handler(PaymentFailedException)
async def payment_failed_handler(request: Request, exc: PaymentFailedException):
return JSONResponse(
status_code=402,
content=exc.to_dict()
)
In ViewSets¶
from strider import ModelViewSet
from strider.exceptions import NotFound, Forbidden
class PostViewSet(ModelViewSet):
model = Post
async def retrieve(self, request, db, **kwargs):
post = await self.get_object(db, **kwargs)
if post.is_private and post.author_id != request.user.id:
raise Forbidden("Cannot view private post")
return await self.serialize(post)
Status Codes¶
| Exception | Status |
|---|---|
BadRequest |
400 |
Unauthorized |
401 |
Forbidden |
403 |
NotFound |
404 |
MethodNotAllowed |
405 |
Conflict |
409 |
UnprocessableEntity |
422 |
ResourceLocked |
423 |
TooManyRequests |
429 |
InternalServerError |
500 |
ServiceUnavailable |
503 |
Next¶
- Validators — Data validation
- ViewSets — CRUD endpoints