Skip to content

Commit d684668

Browse files
authored
Application error support with shared schema (#284)
* Create `ApplicationError` class and update error handler middleware
1 parent d7f5b4c commit d684668

File tree

4 files changed

+79
-3
lines changed

4 files changed

+79
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,4 @@ cython_debug/
285285

286286
credentials.env
287287
volumes/
288+
.qlty

src/common/errors.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class ApplicationError(Exception):
2+
code: str
3+
internal_message: str
4+
public_message: str
5+
metadata: dict | None
6+
7+
def __init__(self, code: str, internal_message: str, public_message: str, metadata: dict | None = None):
8+
"""
9+
Represents an object encapsulating error information with attributes for code,
10+
internal message, public message, and optional metadata.
11+
12+
Attributes:
13+
code (str): A short, unique, and descriptive code identifying the error.
14+
internal_message (str): A detailed message intended for internal use or logging purposes.
15+
public_message (str): A user-friendly message describing the error, suitable for public display.
16+
metadata (dict | None): Optional additional internal information or context regarding the error.
17+
18+
Parameters:
19+
code: A short string representing the unique error code.
20+
internal_message: A string message containing the internal details of the error.
21+
public_message: A string message intended to be displayed to end users.
22+
metadata: An optional dictionary containing extra information about the error.
23+
"""
24+
self.code = code
25+
self.internal_message = internal_message
26+
self.public_message = public_message
27+
self.metadata = metadata

src/http_app/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from common import AppConfig, application_init
99
from common.di_container import Container
10+
from common.errors import ApplicationError
1011
from common.telemetry import instrument_third_party
1112
from http_app import context
1213
from http_app.routes import init_routes
@@ -64,6 +65,23 @@ def init_exception_handlers(app: FastAPI) -> None:
6465
async def add_exception_middleware(request: Request, call_next):
6566
try:
6667
return await call_next(request)
68+
except ApplicationError as e:
69+
logging.exception(
70+
e.internal_message,
71+
extra={
72+
"code": e.code,
73+
"metadata": e.metadata,
74+
},
75+
)
76+
return JSONResponse(
77+
{
78+
"error": {
79+
"message": e.public_message,
80+
"code": e.code,
81+
}
82+
},
83+
status_code=500,
84+
)
6785
except Exception as e:
6886
logging.exception(e)
6987
return JSONResponse({"error": "Internal server error"}, status_code=500)

tests/http_app/test_exception_handlers.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,51 @@
33
from fastapi import FastAPI
44
from fastapi.testclient import TestClient
55

6+
from common.errors import ApplicationError
7+
68

79
@patch("logging.exception")
8-
async def test_exception_is_logged_handler_returns_500(
10+
async def test_unhandled_exception_is_logged_handler_returns_500(
911
mocked_logging_exception: MagicMock,
1012
testapp: FastAPI,
1113
):
1214
my_exc = Exception("Some random exception")
1315

14-
@testapp.get("/ppp")
16+
@testapp.get("/unhandled_exception")
1517
async def fake_endpoint():
1618
raise my_exc
1719

1820
ac = TestClient(app=testapp, base_url="http://test")
19-
response = ac.get("/ppp")
21+
response = ac.get("/unhandled_exception")
2022

2123
assert response.status_code == 500
2224
assert response.json() == {"error": "Internal server error"}
2325
mocked_logging_exception.assert_called_once_with(my_exc)
26+
27+
28+
@patch("logging.exception")
29+
async def test_application_error_is_logged_handler_returns_500(
30+
mocked_logging_exception: MagicMock,
31+
testapp: FastAPI,
32+
):
33+
my_exc = ApplicationError(
34+
public_message="Some random exception",
35+
internal_message="Some random internal exception",
36+
code="ERR_1234567890",
37+
metadata={
38+
"user_id": "1234567890",
39+
},
40+
)
41+
42+
@testapp.get("/application_error")
43+
async def fake_endpoint():
44+
raise my_exc
45+
46+
ac = TestClient(app=testapp, base_url="http://test")
47+
response = ac.get("/application_error")
48+
49+
assert response.status_code == 500
50+
assert response.json() == {"error": {"code": "ERR_1234567890", "message": "Some random exception"}}
51+
mocked_logging_exception.assert_called_once_with(
52+
my_exc.internal_message, extra={"code": my_exc.code, "metadata": my_exc.metadata}
53+
)

0 commit comments

Comments
 (0)