Skip to content

Commit 6a18425

Browse files
committed
Release v0.1.18
- Changes • Added ExceptionCode with built-in standard codes (ISE-500, VAL-422, etc.) • Unified fallback errors to use ExceptionCode.INTERNAL_SERVER_ERROR • Improved validation error handling with consistent responses • Updated docs: added parameters table + clearer examples • Updated packaging (pyproject.toml) with Python 3.10+ requirement and project URLs
1 parent 68014a7 commit 6a18425

File tree

7 files changed

+201
-103
lines changed

7 files changed

+201
-103
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,26 @@ Benchmark scripts and raw Locust reports are available in the [benchmark](https:
410410

411411
## 📜 Changelog
412412

413-
**v0.1.17 (2025-08-10)**
413+
**v0.1.18 - 2025-08-17**
414414

415415
**Initial stable version**
416416

417+
#### Added
418+
- Global logging control (`set_global_log`) with `log` param in `register_exception_handlers`.
419+
- RFC7807 full support with `application/problem+json` responses.
420+
- Automatic injection of `data: null` in OpenAPI error examples.
421+
422+
#### Changed
423+
- Dependency pins relaxed (`>=` instead of strict `==`).
424+
- Docstrings and examples updated (`use_response_model``response_format`).
425+
- Unified error logging (no logs when `log=False`).
426+
427+
#### Fixed
428+
- Fallback middleware now returns HTTP 500 instead of 422 for unexpected errors.
429+
- Traceback scope bug fixed in handlers.
430+
431+
**v0.1.17 (2025-08-10)**
432+
417433
- `RFC 7807` standard support for consistent error responses (`application/problem+json`)
418434

419435
- OpenAPI (Swagger) schema consistency: nullable fields are now explicitly shown for better compatibility

api_exception/__init__.py

Lines changed: 89 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from schemas.rfc7807_model import RFC7807ResponseModel
99
from .logger import logger, add_file_handler
10-
from .exception import APIException, set_default_http_codes, DEFAULT_HTTP_CODES
10+
from .exception import APIException, set_default_http_codes, DEFAULT_HTTP_CODES, set_global_log
1111
from custom_enum.enums import ExceptionCode, ExceptionStatus, BaseExceptionCode, ResponseFormat
1212
from schemas.response_model import ResponseModel
1313
from .response_utils import APIResponse
@@ -27,12 +27,14 @@
2727
"logger",
2828
"add_file_handler",
2929
"APIResponse",
30+
"set_global_log"
3031
]
3132

3233

3334
def register_exception_handlers(app: FastAPI,
3435
response_format: ResponseFormat = ResponseFormat.RESPONSE_MODEL,
3536
use_fallback_middleware: bool = True,
37+
log: bool = True,
3638
log_traceback: bool = True,
3739
log_traceback_unhandled_exception: bool = True,
3840
include_null_data_field_in_openapi: bool = True):
@@ -59,6 +61,10 @@ def register_exception_handlers(app: FastAPI,
5961
If ResponseFormat.RESPONSE_DICTIONARY, returns plain dictionaries.
6062
use_fallback_middleware : bool, default=True
6163
If True, catches ALL unhandled exceptions (runtime errors, etc.) and logs them.
64+
log : bool, default=True
65+
If False, disables **all logging** (including APIException logs and unhandled exceptions).
66+
Overrides `log_exception` flags inside APIException. Useful for production environments
67+
where you want standardized responses but no logging output.
6268
log_traceback : bool, default=True
6369
If True, logs traceback for APIException errors.
6470
log_traceback_unhandled_exception : bool, default=True
@@ -107,7 +113,8 @@ def register_exception_handlers(app: FastAPI,
107113
register_exception_handlers(
108114
app,
109115
use_fallback_middleware=False, # Let FastAPI's default error pages handle unhandled exceptions
110-
use_response_model=ResponseFormat.RESPONSE_MODEL # Return plain dict responses for speed
116+
log=False,
117+
response_format=ResponseFormat.RESPONSE_DICTIONARY # Return plain dict responses for speed
111118
)
112119
```
113120
@@ -124,8 +131,9 @@ def register_exception_handlers(app: FastAPI,
124131
# Register with all customizations
125132
register_exception_handlers(
126133
app,
127-
use_response_model=ResponseFormat.RESPONSE_MODEL,
134+
response_format=ResponseFormat.RESPONSE_MODEL,
128135
use_fallback_middleware=True,
136+
log=True,
129137
log_traceback=True,
130138
log_traceback_unhandled_exception=False, # Don't log tracebacks for runtime errors and/or
131139
# any uncaught errors(db error, 3rd party etc.) (just the message)
@@ -174,7 +182,7 @@ async def user_basic():
174182
# Register with all customizations
175183
register_exception_handlers(
176184
app,
177-
use_response_model=ResponseFormat.RFC7807,
185+
response_format=ResponseFormat.RFC7807,
178186
use_fallback_middleware=True,
179187
log_traceback=True,
180188
log_traceback_unhandled_exception=False, # Don't log tracebacks for runtime errors and/or
@@ -203,116 +211,131 @@ def rfc7807():
203211
)
204212
```
205213
"""
214+
set_global_log(log)
206215

207216
@app.exception_handler(APIException)
208217
async def api_exception_handler(request: Request, exc: APIException):
209-
logger.error(f"Exception handled for path: {request.url.path}")
210-
logger.error(f"Method: {request.method}")
211-
logger.error(f"Client IP: {request.client.host if request.client else 'unknown'}")
212-
if log_traceback:
213-
tb = traceback.format_exc()
214-
logger.error(f"Traceback:\n{tb}")
218+
if log:
219+
logger.error(f"Exception handled for path: {request.url.path}")
220+
logger.error(f"Method: {request.method}")
221+
logger.error(f"Client IP: {request.client.host if request.client else 'unknown'}")
222+
if log_traceback:
223+
tb = traceback.format_exc()
224+
logger.error(f"Traceback:\n{tb}")
215225

216226
if response_format == ResponseFormat.RESPONSE_MODEL:
217227
content = exc.to_response_model().model_dump(exclude_none=False)
228+
media_type = "application/json"
218229
elif response_format == ResponseFormat.RFC7807:
219230
content = exc.to_rfc7807_response().model_dump(exclude_none=False)
231+
media_type = "application/problem+json"
220232
else:
221233
content = exc.to_response()
234+
media_type = "application/json"
222235

223236
return JSONResponse(
224237
status_code=exc.http_status_code,
225238
content=content,
226-
media_type="application/problem+json" if response_format == ResponseFormat.RFC7807 else None
239+
media_type=media_type
227240
)
228241

229242
if use_fallback_middleware:
230243

231244
@app.exception_handler(RequestValidationError)
232-
async def validation_exception_handler(request, exc):
233-
description = exc.errors()[0]["msg"].replace("Value error, ", "") if exc.errors()[0]["msg"].startswith(
234-
"Value error, ") else exc.errors()[0]["msg"]
245+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
246+
try:
247+
first_err = exc.errors()[0]
248+
msg = first_err.get("msg", "Validation error")
249+
except Exception:
250+
msg = "Validation error"
251+
252+
# Mesajı enum'un description'ına gömelim ama "first error message" bilgisini de koruyalım
253+
err = ExceptionCode.VALIDATION_ERROR
254+
description = msg if msg else err.description
235255

236256
if response_format == ResponseFormat.RFC7807:
237257
content = RFC7807ResponseModel(
238-
title="Validation Error",
239-
description=description,
258+
title=err.message,
240259
status=HTTP_422_UNPROCESSABLE_ENTITY,
260+
detail=description,
261+
type=err.rfc7807_type,
262+
instance=err.rfc7807_instance,
241263
).model_dump(exclude_none=False)
242-
else:
264+
media_type = "application/problem+json"
265+
elif response_format == ResponseFormat.RESPONSE_MODEL:
243266
content = ResponseModel(
244267
data=None,
245268
status=ExceptionStatus.FAIL,
246-
message="Validation Error",
247-
error_code="VAL-422",
269+
message=err.message,
270+
error_code=err.error_code,
248271
description=description,
249272
).model_dump(exclude_none=False)
273+
media_type = "application/json"
274+
else:
275+
content = {
276+
"data": None,
277+
"status": ExceptionStatus.FAIL.value,
278+
"message": err.message,
279+
"error_code": err.error_code,
280+
"description": description,
281+
}
282+
media_type = "application/json"
250283

251284
return JSONResponse(
252285
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
253286
content=content,
287+
media_type=media_type,
254288
)
255289

256290
@app.middleware("http")
257-
async def fallback_exception_middleware(request: Request,
258-
call_next: Callable):
259-
"""
260-
Middleware to catch unhandled exceptions and log them.
261-
This middleware acts as a fallback for any unhandled exceptions that occur
262-
during request processing.
263-
It logs the exception details and returns a standardized error response.
264-
This is useful for catching unexpected errors that are not explicitly handled
265-
by the APIException handler.
266-
Parameters:
267-
----------
268-
-----------
269-
request: Request
270-
The incoming request object.
271-
call_next: Callable
272-
The next middleware or endpoint to call.
273-
-----------
274-
275-
Args:
276-
request:
277-
call_next:
278-
279-
Returns:
280-
JSONResponse: A standardized error response with status code 500.
281-
282-
"""
291+
async def fallback_exception_middleware(request: Request, call_next: Callable):
283292
try:
284293
return await call_next(request)
285294
except Exception as e:
286-
tb = traceback.format_exc()
287-
logger.error("⚡ Unhandled Exception Fallback ⚡")
288-
logger.error(f"📌 Path: {request.url.path}")
289-
logger.error(f"📌 Method: {request.method}")
290-
logger.error(f"📌 Client IP: {request.client.host if request.client else 'unknown'}")
291-
logger.error(f"📌 Exception Args: {e.args}")
292-
logger.error(f"📌 Exception: {str(e)}")
293-
294-
if log_traceback_unhandled_exception:
295-
logger.error(f"📌 Traceback:\n{tb}")
295+
if log:
296+
tb = traceback.format_exc()
297+
logger.error("⚡ Unhandled Exception Fallback ⚡")
298+
logger.error(f"📌 Path: {request.url.path}")
299+
logger.error(f"📌 Method: {request.method}")
300+
logger.error(f"📌 Client IP: {request.client.host if request.client else 'unknown'}")
301+
logger.error(f"📌 Exception Args: {e.args}")
302+
logger.error(f"📌 Exception: {str(e)}")
303+
if log_traceback_unhandled_exception:
304+
logger.error(f"📌 Traceback:\n{tb}")
305+
306+
err = ExceptionCode.INTERNAL_SERVER_ERROR
296307

297308
if response_format == ResponseFormat.RFC7807:
298309
content = RFC7807ResponseModel(
299-
title="Validation Error",
300-
description="An unexpected error occurred. Please try again later.",
301-
status=HTTP_422_UNPROCESSABLE_ENTITY,
310+
title=err.message,
311+
status=500,
312+
detail=err.description,
313+
type=err.rfc7807_type,
314+
instance=err.rfc7807_instance,
302315
).model_dump(exclude_none=False)
303-
else:
316+
media_type = "application/problem+json"
317+
elif response_format == ResponseFormat.RESPONSE_MODEL:
304318
content = ResponseModel(
305319
data=None,
306320
status=ExceptionStatus.FAIL,
307-
message="Something went wrong.",
308-
error_code="ISE-500",
309-
description="An unexpected error occurred. Please try again later."
321+
message=err.message,
322+
error_code=err.error_code,
323+
description=err.description,
310324
).model_dump(exclude_none=False)
325+
media_type = "application/json"
326+
else:
327+
content = {
328+
"data": None,
329+
"status": ExceptionStatus.FAIL.value,
330+
"message": err.message,
331+
"error_code": err.error_code,
332+
"description": err.description,
333+
}
334+
media_type = "application/json"
335+
336+
return JSONResponse(status_code=500, content=content, media_type=media_type)
337+
311338

312-
return JSONResponse(
313-
status_code=500,
314-
content=content
315-
)
316339
if include_null_data_field_in_openapi:
317340
"""
318341
Custom OpenAPI schema generator that injects `data: null` into example error responses

api_exception/exception.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from custom_enum.enums import ExceptionCode, ExceptionStatus
88
from schemas.response_model import ResponseModel
99

10+
GLOBAL_LOG: bool = True
11+
12+
13+
def set_global_log(should_log: bool) -> None:
14+
"""Enable/disable all library logging globally."""
15+
global GLOBAL_LOG
16+
GLOBAL_LOG = bool(should_log)
17+
1018

1119

1220
class APIException(Exception):
@@ -68,7 +76,7 @@ def __init__(self,
6876
self.rfc7807_type = error_code.rfc7807_type
6977
self.rfc7807_instance = error_code.rfc7807_instance
7078

71-
if self.log_exception:
79+
if GLOBAL_LOG and self.log_exception:
7280
# Log the exception details
7381
self.__log__()
7482

custom_enum/enums.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,30 @@ class ExceptionCode(BaseExceptionCode):
5252
- A user-friendly message.
5353
- An optional description for more context.
5454
"""
55-
AUTH_LOGIN_FAILED = ("AUTH-1000", "Incorrect username and password.", "Failed authentication attempt.", "https://example.com/problems/authentication-error", "/login")
55+
AUTH_LOGIN_FAILED = ("AUTH-1000", "Incorrect username and password.",
56+
"Failed authentication attempt.",
57+
"https://example.com/problems/authentication-error",
58+
"/login")
5659
EMAIL_ALREADY_TAKEN = (
57-
"RGST-1000", "An account with this email already exists.", "Duplicate email during registration.", "https://example.com/problems/duplicate-registration", "/register")
60+
"RGST-1000",
61+
"An account with this email already exists.",
62+
"Duplicate email during registration.",
63+
"https://example.com/problems/duplicate-registration",
64+
"/register")
65+
INTERNAL_SERVER_ERROR = (
66+
"ISE-500",
67+
"Internal Server Error",
68+
"An unexpected error occurred. Please try again later.",
69+
"https://example.com/problems/internal-server-error",
70+
"/"
71+
)
72+
VALIDATION_ERROR = (
73+
"VAL-422",
74+
"Validation Error",
75+
"Request validation failed. Please check the submitted fields.",
76+
"https://example.com/problems/validation-error",
77+
"/"
78+
)
5879
# Add other exceptions with descriptions as needed
5980

6081

docs/changelog.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,27 @@
33
All notable changes to APIException will be documented here.
44
This project uses *Semantic Versioning*.
55

6-
## [v0.1.17] - 2025-08-11
6+
7+
## [0.1.18] - 2025-08-17
78

89
**Initial stable version**
910

11+
### Added
12+
- Global logging control (`set_global_log`) with `log` param in `register_exception_handlers`.
13+
- RFC7807 full support with `application/problem+json` responses.
14+
- Automatic injection of `data: null` in OpenAPI error examples.
15+
16+
### Changed
17+
- Dependency pins relaxed (`>=` instead of strict `==`).
18+
- Docstrings and examples updated (`use_response_model``response_format`).
19+
- Unified error logging (no logs when `log=False`).
20+
21+
### Fixed
22+
- Fallback middleware now returns HTTP 500 instead of 422 for unexpected errors.
23+
- Traceback scope bug fixed in handlers.
24+
25+
## [v0.1.17] - 2025-08-11
26+
1027
- `RFC 7807` standard support for consistent error responses (`application/problem+json`)
1128

1229
- OpenAPI (Swagger) schema consistency: nullable fields are now explicitly shown for better compatibility

0 commit comments

Comments
 (0)