Ir para o conteúdo

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 → 422
  • UniqueValidationError → 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