From c502e7ab531e03ad15ac66a8516157a93e627a00 Mon Sep 17 00:00:00 2001 From: Igor Magalhaes Date: Fri, 24 Nov 2023 01:07:24 -0300 Subject: [PATCH 1/2] better exception handling --- src/app/api/dependencies.py | 38 +++-------------- src/app/api/exceptions.py | 12 ------ src/app/api/v1/login.py | 4 +- src/app/api/v1/logout.py | 9 ++-- src/app/api/v1/posts.py | 30 ++++++------- src/app/api/v1/rate_limits.py | 23 +++++----- src/app/api/v1/tiers.py | 9 ++-- src/app/api/v1/users.py | 34 +++++++-------- .../{exceptions.py => cache_exceptions.py} | 0 src/app/core/exceptions/http_exceptions.py | 42 +++++++++++++++++++ src/app/core/security.py | 8 ++-- src/app/core/utils/cache.py | 2 +- 12 files changed, 106 insertions(+), 105 deletions(-) delete mode 100644 src/app/api/exceptions.py rename src/app/core/exceptions/{exceptions.py => cache_exceptions.py} (100%) create mode 100644 src/app/core/exceptions/http_exceptions.py diff --git a/src/app/api/dependencies.py b/src/app/api/dependencies.py index 25d367c..29e8b48 100644 --- a/src/app/api/dependencies.py +++ b/src/app/api/dependencies.py @@ -11,7 +11,7 @@ Request ) -from app.api.exceptions import credentials_exception, privileges_exception +from app.core.exceptions.http_exceptions import UnauthorizedException, ForbiddenException, RateLimitException from app.core.db.database import async_get_db from app.core.logger import logging from app.core.schemas import TokenData @@ -28,38 +28,13 @@ DEFAULT_LIMIT = settings.DEFAULT_RATE_LIMIT_LIMIT DEFAULT_PERIOD = settings.DEFAULT_RATE_LIMIT_PERIOD -async def get_current_user( - token: Annotated[str, Depends(oauth2_scheme)], - db: Annotated[AsyncSession, Depends(async_get_db)] -) -> dict: - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username_or_email: str = payload.get("sub") - if username_or_email is None: - raise credentials_exception - token_data = TokenData(username_or_email=username_or_email) - - except JWTError: - raise credentials_exception - - if "@" in username_or_email: - user = await crud_users.get(db=db, email=token_data.username_or_email) - else: - user = await crud_users.get(db=db, username=token_data.username_or_email) - - if user and not user["is_deleted"]: - return user - - raise credentials_exception - - async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[AsyncSession, Depends(async_get_db)] ) -> dict: token_data = await verify_token(token, db) if token_data is None: - raise credentials_exception + raise UnauthorizedException("User not authenticated.") if "@" in token_data.username_or_email: user = await crud_users.get(db=db, email=token_data.username_or_email, is_deleted=False) @@ -69,7 +44,7 @@ async def get_current_user( if user: return user - raise credentials_exception + raise UnauthorizedException("User not authenticated.") async def get_optional_user( @@ -103,7 +78,7 @@ async def get_optional_user( async def get_current_superuser(current_user: Annotated[User, Depends(get_current_user)]) -> dict: if not current_user["is_superuser"]: - raise privileges_exception + raise ForbiddenException("You do not have enough privileges.") return current_user @@ -143,7 +118,4 @@ async def rate_limiter( period=period ) if is_limited: - raise HTTPException( - status_code=429, - detail="Rate limit exceeded" - ) + raise RateLimitException("Rate limit exceeded.") diff --git a/src/app/api/exceptions.py b/src/app/api/exceptions.py deleted file mode 100644 index 10f6126..0000000 --- a/src/app/api/exceptions.py +++ /dev/null @@ -1,12 +0,0 @@ -from fastapi import HTTPException, status - -credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email, username or password", - headers={"WWW-Authenticate": "Bearer"}, -) - -privileges_exception = HTTPException( - status_code=403, - detail="The user doesn't have enough privileges" -) diff --git a/src/app/api/v1/login.py b/src/app/api/v1/login.py index 0871271..210e5e0 100644 --- a/src/app/api/v1/login.py +++ b/src/app/api/v1/login.py @@ -9,7 +9,7 @@ from app.core.db.database import async_get_db from app.core.schemas import Token from app.core.security import ACCESS_TOKEN_EXPIRE_MINUTES, create_access_token, authenticate_user -from app.api.exceptions import credentials_exception +from app.core.exceptions.http_exceptions import UnauthorizedException router = fastapi.APIRouter(tags=["login"]) @@ -24,7 +24,7 @@ async def login_for_access_token( db=db ) if not user: - raise credentials_exception + raise UnauthorizedException("Wrong username, email or password.") access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = await create_access_token( diff --git a/src/app/api/v1/logout.py b/src/app/api/v1/logout.py index b577381..7c6c6ba 100644 --- a/src/app/api/v1/logout.py +++ b/src/app/api/v1/logout.py @@ -1,6 +1,6 @@ from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession from jose import jwt, JWTError @@ -8,6 +8,7 @@ from app.core.db.database import async_get_db from app.core.db.crud_token_blacklist import crud_token_blacklist from app.core.schemas import TokenBlacklistCreate +from app.core.exceptions.http_exceptions import UnauthorizedException router = APIRouter(tags=["login"]) @@ -28,8 +29,4 @@ async def logout( return {"message": "Logged out successfully"} except JWTError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token", - headers={"WWW-Authenticate": "Bearer"}, - ) + raise UnauthorizedException("Invalid token.") diff --git a/src/app/api/v1/posts.py b/src/app/api/v1/posts.py index 433e450..c81d8f8 100644 --- a/src/app/api/v1/posts.py +++ b/src/app/api/v1/posts.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import Request, Depends, HTTPException +from fastapi import Request, Depends from sqlalchemy.ext.asyncio import AsyncSession import fastapi @@ -10,7 +10,7 @@ from app.core.db.database import async_get_db from app.crud.crud_posts import crud_posts from app.crud.crud_users import crud_users -from app.api.exceptions import privileges_exception +from app.core.exceptions.http_exceptions import NotFoundException, ForbiddenException from app.core.utils.cache import cache from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset @@ -26,10 +26,10 @@ async def write_post( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if current_user["id"] != db_user["id"]: - raise privileges_exception + raise ForbiddenException() post_internal_dict = post.model_dump() post_internal_dict["created_by_user_id"] = db_user["id"] @@ -53,7 +53,7 @@ async def read_posts( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if not db_user: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") posts_data = await crud_posts.get_multi( db=db, @@ -81,11 +81,11 @@ async def read_post( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, created_by_user_id=db_user["id"], is_deleted=False) if db_post is None: - raise HTTPException(status_code=404, detail="Post not found") + raise NotFoundException("Post not found") return db_post @@ -106,14 +106,14 @@ async def patch_post( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if current_user["id"] != db_user["id"]: - raise privileges_exception + raise ForbiddenException() db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False) if db_post is None: - raise HTTPException(status_code=404, detail="Post not found") + raise NotFoundException("Post not found") await crud_posts.update(db=db, object=values, id=id) return {"message": "Post updated"} @@ -134,14 +134,14 @@ async def erase_post( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if current_user["id"] != db_user["id"]: - raise privileges_exception + raise ForbiddenException() db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False) if db_post is None: - raise HTTPException(status_code=404, detail="Post not found") + raise NotFoundException("Post not found") await crud_posts.delete(db=db, db_row=db_post, id=id) @@ -162,11 +162,11 @@ async def erase_db_post( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False) if db_post is None: - raise HTTPException(status_code=404, detail="Post not found") + raise NotFoundException("Post not found") await crud_posts.db_delete(db=db, id=id) return {"message": "Post deleted from the database"} diff --git a/src/app/api/v1/rate_limits.py b/src/app/api/v1/rate_limits.py index 82a88e5..4f04ec8 100644 --- a/src/app/api/v1/rate_limits.py +++ b/src/app/api/v1/rate_limits.py @@ -7,6 +7,7 @@ from app.api.dependencies import get_current_superuser from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset from app.core.db.database import async_get_db +from app.core.exceptions.http_exceptions import NotFoundException, DuplicateValueException, RateLimitException from app.crud.crud_rate_limit import crud_rate_limits from app.crud.crud_tier import crud_tiers from app.schemas.rate_limit import ( @@ -27,14 +28,14 @@ async def write_rate_limit( ): db_tier = await crud_tiers.get(db=db, name=tier_name) if not db_tier: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") rate_limit_internal_dict = rate_limit.model_dump() rate_limit_internal_dict["tier_id"] = db_tier["id"] db_rate_limit = await crud_rate_limits.exists(db=db, name=rate_limit_internal_dict["name"]) if db_rate_limit: - raise HTTPException(status_code=400, detail="Rate Limit Name not available") + raise DuplicateValueException("Rate Limit Name not available") rate_limit_internal = RateLimitCreateInternal(**rate_limit_internal_dict) return await crud_rate_limits.create(db=db, object=rate_limit_internal) @@ -50,7 +51,7 @@ async def read_rate_limits( ): db_tier = await crud_tiers.get(db=db, name=tier_name) if not db_tier: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") rate_limits_data = await crud_rate_limits.get_multi( db=db, @@ -76,7 +77,7 @@ async def read_rate_limit( ): db_tier = await crud_tiers.get(db=db, name=tier_name) if not db_tier: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") db_rate_limit = await crud_rate_limits.get( db=db, @@ -85,7 +86,7 @@ async def read_rate_limit( id=id ) if db_rate_limit is None: - raise HTTPException(status_code=404, detail="Rate Limit not found") + raise NotFoundException("Rate Limit not found") return db_rate_limit @@ -100,7 +101,7 @@ async def patch_rate_limit( ): db_tier = await crud_tiers.get(db=db, name=tier_name) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") db_rate_limit = await crud_rate_limits.get( db=db, @@ -109,7 +110,7 @@ async def patch_rate_limit( id=id ) if db_rate_limit is None: - raise HTTPException(status_code=404, detail="Rate Limit not found") + raise NotFoundException("Rate Limit not found") db_rate_limit_path = await crud_rate_limits.exists( db=db, @@ -117,11 +118,11 @@ async def patch_rate_limit( path=values.path ) if db_rate_limit_path is not None: - raise HTTPException(status_code=404, detail="There is already a rate limit for this path") + raise DuplicateValueException("There is already a rate limit for this path") db_rate_limit_name = await crud_rate_limits.exists(db=db) if db_rate_limit_path is not None: - raise HTTPException(status_code=404, detail="There is already a rate limit with this name") + raise DuplicateValueException("There is already a rate limit with this name") await crud_rate_limits.update(db=db, object=values, id=db_rate_limit["id"]) return {"message": "Rate Limit updated"} @@ -136,7 +137,7 @@ async def erase_rate_limit( ): db_tier = await crud_tiers.get(db=db, name=tier_name) if not db_tier: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") db_rate_limit = await crud_rate_limits.get( db=db, @@ -145,7 +146,7 @@ async def erase_rate_limit( id=id ) if db_rate_limit is None: - raise HTTPException(status_code=404, detail="Rate Limit not found") + raise RateLimitException("Rate Limit not found") await crud_rate_limits.delete(db=db, db_row=db_rate_limit, id=db_rate_limit["id"]) return {"message": "Rate Limit deleted"} diff --git a/src/app/api/v1/tiers.py b/src/app/api/v1/tiers.py index b5cfd87..5a1046e 100644 --- a/src/app/api/v1/tiers.py +++ b/src/app/api/v1/tiers.py @@ -12,6 +12,7 @@ ) from app.api.dependencies import get_current_superuser from app.core.db.database import async_get_db +from app.core.exceptions.http_exceptions import DuplicateValueException, NotFoundException from app.crud.crud_tier import crud_tiers from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset @@ -26,7 +27,7 @@ async def write_tier( tier_internal_dict = tier.model_dump() db_tier = await crud_tiers.exists(db=db, name=tier_internal_dict["name"]) if db_tier: - raise HTTPException(status_code=400, detail="Tier Name not available") + raise DuplicateValueException("Tier Name not available") tier_internal = TierCreateInternal(**tier_internal_dict) return await crud_tiers.create(db=db, object=tier_internal) @@ -61,7 +62,7 @@ async def read_tier( ): db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") return db_tier @@ -75,7 +76,7 @@ async def patch_tier( ): db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") await crud_tiers.update(db=db, object=values, name=name) return {"message": "Tier updated"} @@ -89,7 +90,7 @@ async def erase_tier( ): db_tier = await crud_tiers.get(db=db, schema_to_select=TierRead, name=name) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") await crud_tiers.delete(db=db, db_row=db_tier, name=name) return {"message": "Tier deleted"} diff --git a/src/app/api/v1/users.py b/src/app/api/v1/users.py index 74b8508..32b358e 100644 --- a/src/app/api/v1/users.py +++ b/src/app/api/v1/users.py @@ -6,7 +6,7 @@ import fastapi from app.api.dependencies import get_current_user, get_current_superuser -from app.api.exceptions import privileges_exception +from app.core.exceptions.http_exceptions import DuplicateValueException, NotFoundException, ForbiddenException from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset from app.core.db.database import async_get_db from app.core.security import get_password_hash @@ -27,11 +27,11 @@ async def write_user( ): email_row = await crud_users.exists(db=db, email=user.email) if email_row: - raise HTTPException(status_code=400, detail="Email is already registered") + raise DuplicateValueException("Email is already registered") username_row = await crud_users.exists(db=db, username=user.username) if username_row: - raise HTTPException(status_code=400, detail="Username not available") + raise DuplicateValueException("Username not available") user_internal_dict = user.model_dump() user_internal_dict["hashed_password"] = get_password_hash(password=user_internal_dict["password"]) @@ -74,7 +74,7 @@ async def read_users_me( async def read_user(request: Request, username: str, db: Annotated[AsyncSession, Depends(async_get_db)]): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") return db_user @@ -89,20 +89,20 @@ async def patch_user( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if db_user["username"] != current_user["username"]: - raise privileges_exception + raise ForbiddenException() if values.username != db_user["username"]: existing_username = await crud_users.exists(db=db, username=values.username) if existing_username: - raise HTTPException(status_code=400, detail="Username not available") + raise DuplicateValueException("Username not available") if values.email != db_user["email"]: existing_email = await crud_users.exists(db=db, email=values.email) if existing_email: - raise HTTPException(status_code=400, detail="Email is already registered") + raise DuplicateValueException("Email is already registered") await crud_users.update(db=db, object=values, username=username) return {"message": "User updated"} @@ -117,10 +117,10 @@ async def erase_user( ): db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username) if not db_user: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if username != current_user["username"]: - raise privileges_exception + raise ForbiddenException() await crud_users.delete(db=db, db_row=db_user, username=username) return {"message": "User deleted"} @@ -134,7 +134,7 @@ async def erase_db_user( ): db_user = await crud_users.exists(db=db, username=username) if not db_user: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") db_user = await crud_users.db_delete(db=db, username=username) return {"message": "User deleted from the database"} @@ -148,7 +148,7 @@ async def read_user_rate_limits( ): db_user = await crud_users.get(db=db, username=username, schema_to_select=UserRead) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") if db_user["tier_id"] is None: db_user["tier_rate_limits"] = [] @@ -156,7 +156,7 @@ async def read_user_rate_limits( db_tier = await crud_tiers.get(db=db, id=db_user["tier_id"]) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") db_rate_limits = await crud_rate_limits.get_multi( db=db, @@ -176,11 +176,11 @@ async def read_user_tier( ): db_user = await crud_users.get(db=db, username=username, schema_to_select=UserRead) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") db_tier = await crud_tiers.exists(db=db, id=db_user["tier_id"]) if not db_tier: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") joined = await crud_users.get_joined( db=db, @@ -203,11 +203,11 @@ async def patch_user_tier( ): db_user = await crud_users.get(db=db, username=username, schema_to_select=UserRead) if db_user is None: - raise HTTPException(status_code=404, detail="User not found") + raise NotFoundException("User not found") db_tier = await crud_tiers.get(db=db, id=values.tier_id) if db_tier is None: - raise HTTPException(status_code=404, detail="Tier not found") + raise NotFoundException("Tier not found") await crud_users.update(db=db, object=values, username=username) return {"message": f"User {db_user['name']} Tier updated"} diff --git a/src/app/core/exceptions/exceptions.py b/src/app/core/exceptions/cache_exceptions.py similarity index 100% rename from src/app/core/exceptions/exceptions.py rename to src/app/core/exceptions/cache_exceptions.py diff --git a/src/app/core/exceptions/http_exceptions.py b/src/app/core/exceptions/http_exceptions.py new file mode 100644 index 0000000..5c93742 --- /dev/null +++ b/src/app/core/exceptions/http_exceptions.py @@ -0,0 +1,42 @@ +from fastapi import HTTPException, status + +class CustomException(HTTPException): + def __init__(self, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, detail: str | None = None): + if not detail: + detail = status_code.description + super().__init__(status_code=status_code, detail=detail) + + +class BadRequestException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) + + +class NotFoundException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_404_NOT_FOUND, detail=detail) + + +class ForbiddenException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail) + + +class UnauthorizedException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail) + + +class UnprocessableEntityException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail) + + +class DuplicateValueException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=detail) + + +class RateLimitException(CustomException): + def __init__(self, detail: str | None = None): + super().__init__(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=detail) diff --git a/src/app/core/security.py b/src/app/core/security.py index a55c21a..9c85671 100644 --- a/src/app/core/security.py +++ b/src/app/core/security.py @@ -25,11 +25,11 @@ def get_password_hash(password): async def authenticate_user(username_or_email: str, password: str, db: AsyncSession): if "@" in username_or_email: - db_user = await crud_users.get(db=db, email=username_or_email) - else: - db_user = await crud_users.get(db=db, username=username_or_email) + db_user = await crud_users.get(db=db, email=username_or_email, is_deleted=False) + else: + db_user = await crud_users.get(db=db, username=username_or_email, is_deleted=False) - if (not db_user) or (db_user["is_deleted"]): + if not db_user: db_user = False elif not await verify_password(password, db_user["hashed_password"]): diff --git a/src/app/core/utils/cache.py b/src/app/core/utils/cache.py index 0f7ef36..ef3f465 100644 --- a/src/app/core/utils/cache.py +++ b/src/app/core/utils/cache.py @@ -7,7 +7,7 @@ from fastapi.encoders import jsonable_encoder from redis.asyncio import Redis, ConnectionPool -from app.core.exceptions.exceptions import CacheIdentificationInferenceError, InvalidRequestError +from app.core.exceptions.cache_exceptions import CacheIdentificationInferenceError, InvalidRequestError pool: ConnectionPool | None = None client: Redis | None = None From a1de9461f72cc8431e52bc476fc503cf9654a5e1 Mon Sep 17 00:00:00 2001 From: Igor Magalhaes Date: Fri, 24 Nov 2023 01:26:12 -0300 Subject: [PATCH 2/2] Documentation updated --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75e0b11..ed96b40 100644 --- a/README.md +++ b/README.md @@ -430,7 +430,6 @@ First, you may want to take a look at the project structure and understand what │ ├── api # Folder containing API-related logic. │ │ ├── __init__.py │ │ ├── dependencies.py # Defines dependencies for use across API endpoints. - │ │ ├── exceptions.py # Custom exceptions for the API. │ │ ├── paginated.py # Utilities for API response pagination. │ │ │ │ │ └── v1 # Version 1 of the API. @@ -460,7 +459,8 @@ First, you may want to take a look at the project structure and understand what │ │ │ │ │ ├── exceptions # Custom exception classes. │ │ │ ├── __init__.py - │ │ │ └── exceptions.py # Definitions of custom exceptions. + │ │ │ ├── cache_exceptions.py # Exceptions related to cache operations. + │ │ │ └── http_exceptions.py # HTTP-related exceptions. │ │ │ │ │ └── utils # Utility functions and helpers. │ │ ├── __init__.py @@ -890,6 +890,33 @@ async def read_entities( ) ``` +#### 5.7.2 HTTP Exceptions + +To add exceptions you may just import from `app/core/exceptions/http_exceptions` and optionally add a detail: + +```python +from app.core.exceptions.http_exceptions import NotFoundException + +# If you want to specify the detail, just add the message +if not user: + raise NotFoundException("User not found") + +# Or you may just use the default message +if not post: + raise NotFoundException() +``` + +**The predefined possibilities in http_exceptions are the following:** +- `CustomException`: 500 internal error +- `BadRequestException`: 400 bad request +- `NotFoundException`: 404 not found +- `ForbiddenException`: 403 forbidden +- `UnauthorizedException`: 401 unauthorized +- `UnprocessableEntityException`: 422 unprocessable entity +- `DuplicateValueException`: 422 unprocessable entity +- `RateLimitException`: 429 too many requests + + ### 5.8 Caching The `cache` decorator allows you to cache the results of FastAPI endpoint functions, enhancing response times and reducing the load on your application by storing and retrieving data in a cache.