-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance CI workflow and implement global exception handling #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| from . import cache, quote_service, quote_repository, security | ||
| from . import cache, config, database, security | ||
|
|
||
| __all__ = ["cache", "quote_service", "quote_repository", "security"] | ||
| __all__ = ["cache", "config", "database", "security"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from . import custom_exceptions, handlers | ||
|
|
||
| __all__ = ["custom_exceptions", "handlers"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| class ResourceNotFoundException(Exception): | ||
| def __init__(self, message: str) -> None: | ||
| self.message = message | ||
|
|
||
|
|
||
| class DomainValidationException(Exception): | ||
| def __init__(self, message: str) -> None: | ||
| self.message = message | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,67 @@ | ||||||||||||||||||||||||||||
| from http import HTTPStatus | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from fastapi import Request | ||||||||||||||||||||||||||||
| from fastapi.exceptions import RequestValidationError, HTTPException | ||||||||||||||||||||||||||||
| from fastapi.responses import JSONResponse | ||||||||||||||||||||||||||||
| from loguru import logger | ||||||||||||||||||||||||||||
| from schemas.error_schema import ErrorResponse | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from .custom_exceptions import DomainValidationException, ResourceNotFoundException | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _json_error_response(error: ErrorResponse) -> JSONResponse: | ||||||||||||||||||||||||||||
| return JSONResponse( | ||||||||||||||||||||||||||||
| status_code=error.status_code, content=error.model_dump(exclude_none=True) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def resource_not_found_handler( | ||||||||||||||||||||||||||||
| _: Request, exc: ResourceNotFoundException | ||||||||||||||||||||||||||||
| ) -> JSONResponse: | ||||||||||||||||||||||||||||
| return _json_error_response( | ||||||||||||||||||||||||||||
| ErrorResponse.from_http_status( | ||||||||||||||||||||||||||||
| status_code=HTTPStatus.NOT_FOUND, message=exc.message | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def domain_validation_handler( | ||||||||||||||||||||||||||||
| _: Request, exc: DomainValidationException | ||||||||||||||||||||||||||||
| ) -> JSONResponse: | ||||||||||||||||||||||||||||
| return _json_error_response( | ||||||||||||||||||||||||||||
| ErrorResponse.from_http_status( | ||||||||||||||||||||||||||||
| status_code=HTTPStatus.BAD_REQUEST, message=exc.message | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def request_validation_handler( | ||||||||||||||||||||||||||||
| _: Request, exc: RequestValidationError | ||||||||||||||||||||||||||||
| ) -> JSONResponse: | ||||||||||||||||||||||||||||
| return _json_error_response( | ||||||||||||||||||||||||||||
| ErrorResponse.from_http_status( | ||||||||||||||||||||||||||||
| status_code=HTTPStatus.UNPROCESSABLE_ENTITY, | ||||||||||||||||||||||||||||
| message="Erro de validação nos dados enviados", | ||||||||||||||||||||||||||||
| details=exc.errors(), | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async def http_handler(_: Request, exc: HTTPException) -> JSONResponse: | ||||||||||||||||||||||||||||
| return _json_error_response( | ||||||||||||||||||||||||||||
| ErrorResponse.from_http_status( | ||||||||||||||||||||||||||||
| HTTPStatus(exc.status_code), | ||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+53
|
||||||||||||||||||||||||||||
| return _json_error_response( | |
| ErrorResponse.from_http_status( | |
| HTTPStatus(exc.status_code), | |
| status = ( | |
| HTTPStatus(exc.status_code) | |
| if exc.status_code in HTTPStatus._value2member_map_ | |
| else HTTPStatus.INTERNAL_SERVER_ERROR | |
| ) | |
| return _json_error_response( | |
| ErrorResponse.from_http_status( | |
| status, |
Copilot
AI
Mar 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http_handler passes exc.detail into ErrorResponse.message, but HTTPException.detail can be non-string (e.g., dict/list). That will raise a Pydantic validation error inside the exception handler and can prevent the standardized error response from being returned. Consider ensuring message is always a string (e.g., str(exc.detail) when not a string) and optionally put the original detail payload into details.
| return _json_error_response( | |
| ErrorResponse.from_http_status( | |
| HTTPStatus(exc.status_code), | |
| message=exc.detail, | |
| detail = exc.detail | |
| message = detail if isinstance(detail, str) else str(detail) | |
| details = None if isinstance(detail, str) else detail | |
| return _json_error_response( | |
| ErrorResponse.from_http_status( | |
| HTTPStatus(exc.status_code), | |
| message=message, | |
| details=details, |
WillianSilva51 marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,21 @@ | |
|
|
||
| from core.database import init_db | ||
| from fastapi import FastAPI | ||
| from fastapi.exceptions import RequestValidationError, HTTPException | ||
| from routers.quotes import api_router as quotes_router | ||
|
|
||
| from exceptions.custom_exceptions import ( | ||
| DomainValidationException, | ||
| ResourceNotFoundException, | ||
| ) | ||
| from exceptions.handlers import ( | ||
| http_handler, | ||
| domain_validation_handler, | ||
| resource_not_found_handler, | ||
| request_validation_handler, | ||
| global_exception_handler, | ||
| ) | ||
|
|
||
| tags_metadata = [ | ||
| { | ||
| "name": "Frases", | ||
|
|
@@ -13,7 +26,7 @@ | |
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def lifespan(app: FastAPI): | ||
| async def lifespan(_: FastAPI): | ||
| await init_db() | ||
| yield | ||
|
|
||
|
|
@@ -47,4 +60,10 @@ async def lifespan(app: FastAPI): | |
| lifespan=lifespan, | ||
| ) | ||
|
|
||
| app.add_exception_handler(HTTPException, http_handler) # type: ignore | ||
| app.add_exception_handler(DomainValidationException, domain_validation_handler) # type: ignore | ||
| app.add_exception_handler(ResourceNotFoundException, resource_not_found_handler) # type: ignore | ||
| app.add_exception_handler(RequestValidationError, request_validation_handler) # type: ignore | ||
| app.add_exception_handler(Exception, global_exception_handler) | ||
|
Comment on lines
+63
to
+67
|
||
|
|
||
| app.include_router(quotes_router, prefix="/api") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| from . import pagination, quote_schema | ||
| from . import error_schema, pagination, quote_schema | ||
|
|
||
| __all__ = ["pagination", "quote_schema"] | ||
| __all__ = ["error_schema", "pagination", "quote_schema"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| from pydantic import BaseModel | ||
| from http import HTTPStatus | ||
| from typing import Any, Sequence | ||
|
|
||
|
|
||
| class ErrorResponse(BaseModel): | ||
| status_code: HTTPStatus | ||
| error: str | ||
| message: str | ||
| details: Sequence[Any] | dict[str, Any] | list[Any] | None = None | ||
|
|
||
| @classmethod | ||
| def from_http_status( | ||
| cls, | ||
| status_code: HTTPStatus, | ||
| message: str, | ||
| details: Sequence[Any] | dict[str, Any] | list[Any] | None = None, | ||
| error: str | None = None, | ||
| ) -> "ErrorResponse": | ||
| return ErrorResponse( | ||
| status_code=status_code, | ||
| error=error or status_code.phrase, | ||
| message=message, | ||
| details=details, | ||
| ) | ||
|
Comment on lines
+12
to
+25
|
||
Uh oh!
There was an error while loading. Please reload this page.