Skip to content

Commit b7a91fb

Browse files
committed
feat: introduced a first version of an error handler
1 parent df5a45d commit b7a91fb

File tree

6 files changed

+207
-34
lines changed

6 files changed

+207
-34
lines changed

app/auth.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from jwt import PyJWKClient
77
from loguru import logger
88

9+
from app.error import AuthException
10+
911
from .config.settings import settings
1012

1113
# Keycloak OIDC info
@@ -37,9 +39,9 @@ def _decode_token(token: str):
3739
)
3840
return payload
3941
except Exception:
40-
raise HTTPException(
41-
status_code=status.HTTP_401_UNAUTHORIZED,
42-
detail="Could not validate credentials",
42+
raise AuthException(
43+
http_status=status.HTTP_401_UNAUTHORIZED,
44+
message="Could not validate credentials!",
4345
)
4446

4547

app/database/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def get_db():
3535
yield db
3636
db.commit()
3737
except Exception:
38-
logger.exception("An error occurred during database retrieval")
38+
logger.error("An error occurred during database retrieval")
3939
db.rollback()
4040
raise
4141
finally:

app/error.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import Any, Dict, Optional
2+
from fastapi import status
3+
from pydantic import BaseModel
4+
5+
6+
class ErrorResponse(BaseModel):
7+
status: str = "error"
8+
error_code: str
9+
message: str
10+
details: Optional[Dict[str, Any]] = None
11+
request_id: Optional[str] = None
12+
13+
14+
class DispatcherException(Exception):
15+
"""
16+
Base domain exception for the APEx Dispatch API.
17+
"""
18+
19+
http_status: int = status.HTTP_400_BAD_REQUEST
20+
error_code: str = "APEX_ERROR"
21+
message: str = "An error occurred."
22+
details: Optional[Dict[str, Any]] = None
23+
24+
def __init__(
25+
self,
26+
message: Optional[str] = None,
27+
error_code: Optional[str] = None,
28+
http_status: Optional[int] = None,
29+
details: Optional[Dict[str, Any]] = None,
30+
):
31+
if message:
32+
self.message = message
33+
if error_code:
34+
self.error_code = error_code
35+
if http_status:
36+
self.http_status = http_status
37+
if details:
38+
self.details = details
39+
40+
def __str__(self):
41+
return f"{self.error_code}: {self.message}"
42+
43+
44+
class AuthException(DispatcherException):
45+
def __init__(
46+
self,
47+
http_status: Optional[int] = status.HTTP_401_UNAUTHORIZED,
48+
message: Optional[Dict[str, Any]] = "Authentication failed.",
49+
):
50+
super().__init__(message, "AUTHENTICATION_FAILED", http_status)
51+
52+
53+
class JobNotFoundException(DispatcherException):
54+
http_status: int = status.HTTP_404_NOT_FOUND
55+
error_code: str = "JOB_NOT_FOUND"
56+
message: str = "The requested job was not found."
57+
58+
59+
class InternalException(DispatcherException):
60+
http_status: int = status.HTTP_500_INTERNAL_SERVER_ERROR
61+
error_code: str = "INTERNAL_ERROR"
62+
message: str = "An internal server error occurred."

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from fastapi.middleware.cors import CORSMiddleware
33

44
from app.middleware.correlation_id import add_correlation_id
5+
from app.middleware.error_handling import register_exception_handlers
56
from app.platforms.dispatcher import load_processing_platforms
67
from app.services.tiles.base import load_grids
78
from app.config.logger import setup_logging
@@ -28,6 +29,7 @@
2829
)
2930

3031
app.middleware("http")(add_correlation_id)
32+
register_exception_handlers(app)
3133

3234
# include routers
3335
app.include_router(tiles.router)

app/middleware/error_handling.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from fastapi import Request, status
2+
from fastapi.responses import JSONResponse
3+
from app.error import DispatcherException, ErrorResponse
4+
from app.middleware.correlation_id import correlation_id_ctx
5+
from loguru import logger
6+
7+
8+
def get_dispatcher_error_response(
9+
exc: DispatcherException, request_id: str
10+
) -> ErrorResponse:
11+
return ErrorResponse(
12+
error_code=exc.error_code,
13+
message=exc.message,
14+
details=exc.details,
15+
request_id=request_id,
16+
)
17+
18+
19+
async def dispatch_exception_handler(request: Request, exc: DispatcherException):
20+
21+
content = get_dispatcher_error_response(exc, correlation_id_ctx.get())
22+
logger.exception(f"DispatcherException raised: {exc.message}")
23+
return JSONResponse(status_code=exc.http_status, content=content.dict())
24+
25+
26+
async def generic_exception_handler(request: Request, exc: Exception):
27+
28+
# DO NOT expose internal exceptions to the client
29+
content = ErrorResponse(
30+
error_code="INTERNAL_SERVER_ERROR",
31+
message="An unexpected error occurred.",
32+
details=None,
33+
request_id=correlation_id_ctx.get(),
34+
)
35+
36+
# Log exception to server logs for debugging
37+
print(f"[ERROR] Request ID: {exc}")
38+
39+
return JSONResponse(
40+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=content.dict()
41+
)
42+
43+
44+
def register_exception_handlers(app):
45+
"""
46+
Call this in main.py after creating the FastAPI() instance.
47+
"""
48+
49+
app.add_exception_handler(DispatcherException, dispatch_exception_handler)
50+
# app.add_exception_handler(RequestValidationError, validation_exception_handler)
51+
app.add_exception_handler(Exception, generic_exception_handler)

app/routers/unit_jobs.py

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55

66
from app.auth import oauth2_scheme
77
from app.database.db import get_db
8+
from app.error import (
9+
AuthException,
10+
DispatcherException,
11+
ErrorResponse,
12+
InternalException,
13+
JobNotFoundException,
14+
)
15+
from app.middleware.error_handling import get_dispatcher_error_response
816
from app.schemas.enum import OutputFormatEnum, ProcessTypeEnum
917
from app.schemas.unit_job import (
1018
BaseJobRequest,
@@ -30,6 +38,19 @@
3038
status_code=status.HTTP_201_CREATED,
3139
tags=["Unit Jobs"],
3240
summary="Create a new processing job",
41+
responses={
42+
InternalException.http_status: {
43+
"description": "Internal server error",
44+
"model": ErrorResponse,
45+
"content": {
46+
"application/json": {
47+
"example": get_dispatcher_error_response(
48+
InternalException(), "request-id"
49+
)
50+
}
51+
},
52+
},
53+
},
3354
)
3455
async def create_unit_job(
3556
payload: Annotated[
@@ -105,65 +126,100 @@ async def create_unit_job(
105126
"""Create a new processing job with the provided data."""
106127
try:
107128
return await create_processing_job(token, db, payload)
108-
except HTTPException as e:
109-
raise e
129+
except DispatcherException as de:
130+
raise de
110131
except Exception as e:
111-
logger.exception(f"Error creating processing job: {e}")
112-
raise HTTPException(
113-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114-
detail=f"An error occurred while creating the processing job: {e}",
132+
logger.error(f"Error creating processing job: {e}")
133+
raise InternalException(
134+
message="An error occurred while creating processing job."
115135
)
116136

117137

118138
@router.get(
119139
"/unit_jobs/{job_id}",
120140
tags=["Unit Jobs"],
121-
responses={404: {"description": "Processing job not found"}},
141+
responses={
142+
JobNotFoundException.http_status: {
143+
"description": "Job not found",
144+
"model": ErrorResponse,
145+
"content": {
146+
"application/json": {
147+
"example": get_dispatcher_error_response(
148+
JobNotFoundException(), "request-id"
149+
)
150+
}
151+
},
152+
},
153+
InternalException.http_status: {
154+
"description": "Internal server error",
155+
"model": ErrorResponse,
156+
"content": {
157+
"application/json": {
158+
"example": get_dispatcher_error_response(
159+
InternalException(), "request-id"
160+
)
161+
}
162+
},
163+
},
164+
},
122165
)
123166
async def get_job(
124167
job_id: int, db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
125168
) -> ProcessingJob:
126169
try:
127170
job = await get_processing_job_by_user_id(token, db, job_id)
128171
if not job:
129-
logger.error(f"Processing job {job_id} not found")
130-
raise HTTPException(
131-
status_code=404,
132-
detail=f"Processing job {job_id} not found",
133-
)
172+
raise JobNotFoundException()
134173
return job
135-
except HTTPException as e:
136-
raise e
174+
except DispatcherException as de:
175+
raise de
137176
except Exception as e:
138-
logger.exception(f"Error retrieving processing job {job_id}: {e}")
139-
raise HTTPException(
140-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
141-
detail=f"An error occurred while retrieving processing job {job_id}: {e}",
177+
logger.error(f"Error retrieving processing job {job_id}: {e}")
178+
raise InternalException(
179+
message="An error occurred while retrieving the processing job."
142180
)
143181

144182

145183
@router.get(
146184
"/unit_jobs/{job_id}/results",
147185
tags=["Unit Jobs"],
148-
responses={404: {"description": "Processing job not found"}},
186+
responses={
187+
JobNotFoundException.http_status: {
188+
"description": "Job not found",
189+
"model": ErrorResponse,
190+
"content": {
191+
"application/json": {
192+
"example": get_dispatcher_error_response(
193+
JobNotFoundException(), "request-id"
194+
)
195+
}
196+
},
197+
},
198+
InternalException.http_status: {
199+
"description": "Internal server error",
200+
"model": ErrorResponse,
201+
"content": {
202+
"application/json": {
203+
"example": get_dispatcher_error_response(
204+
InternalException(), "request-id"
205+
)
206+
}
207+
},
208+
},
209+
},
149210
)
150211
async def get_job_results(
151212
job_id: int, db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
152213
) -> Collection | None:
153214
try:
154215
result = await get_processing_job_results(token, db, job_id)
155216
if not result:
156-
logger.error(f"Result for processing job {job_id} not found")
157-
raise HTTPException(
158-
status_code=404,
159-
detail=f"Result for processing job {job_id} not found",
160-
)
217+
raise JobNotFoundException()
161218
return result
162-
except HTTPException as e:
163-
raise e
219+
except DispatcherException as de:
220+
raise de
164221
except Exception as e:
165-
logger.exception(f"Error getting results for processing job {job_id}: {e}")
166-
raise HTTPException(
167-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
168-
detail=f"An error occurred while retrieving results for processing job {job_id}: {e}",
222+
logger.error(f"Error getting results for processing job {job_id}: {e}")
223+
raise InternalException(
224+
message="An error occurred while retrieving processing job results."
169225
)

0 commit comments

Comments
 (0)