-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi_errors.py
More file actions
116 lines (97 loc) · 3.52 KB
/
Copy pathapi_errors.py
File metadata and controls
116 lines (97 loc) · 3.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""Unified API error envelope.
Every JSON route should either:
* return a plain dict / response (treated as 200 OK), or
* raise :class:`ApiError` with a code, message, and HTTP status.
The Flask ``errorhandler`` registered in :mod:`app` translates ``ApiError``
into a uniform response shape so the frontend can rely on one error contract.
Response shape
--------------
``{"status": "error", "code": "<symbolic>", "message": "<human>", "details": {...}}``
"""
from __future__ import annotations
from typing import Any
class ApiError(Exception):
"""Structured API error.
Parameters
----------
message
Human-readable message safe to show to end users.
code
Short symbolic identifier (e.g. ``"ticker_required"``,
``"yfinance_unavailable"``). Stable across versions; frontend may
switch on it.
status
HTTP status code (default ``400``).
details
Optional structured payload echoed back to the client.
"""
__slots__ = ("message", "code", "status", "details")
def __init__(
self,
message: str,
code: str = "bad_request",
status: int = 400,
details: dict[str, Any] | None = None,
) -> None:
super().__init__(message)
self.message = message
self.code = code
self.status = status
self.details = details or {}
def to_payload(self) -> dict[str, Any]:
out: dict[str, Any] = {
"status": "error",
"code": self.code,
"message": self.message,
}
if self.details:
out["details"] = self.details
return out
def install(app) -> None:
"""Register error handlers on a Flask app.
Catches both :class:`ApiError` and unhandled exceptions, returning a
uniform JSON envelope. Non-API HTML routes are unaffected because the
handler only fires for routes that raised — static templates render
normally.
"""
from flask import jsonify, request
@app.errorhandler(ApiError)
def _handle_api_error(err: ApiError): # type: ignore[no-redef]
return jsonify(err.to_payload()), err.status
@app.errorhandler(404)
def _handle_404(err): # type: ignore[no-redef]
# Only JSON-ify API paths; let HTML 404 fall through.
if request.path.startswith("/api/"):
return (
jsonify(
{
"status": "error",
"code": "not_found",
"message": f"No route: {request.path}",
}
),
404,
)
return err
@app.errorhandler(Exception)
def _handle_unexpected(err: Exception): # type: ignore[no-redef]
# Re-raise HTTPExceptions so Flask handles them normally.
from werkzeug.exceptions import HTTPException
if isinstance(err, HTTPException):
return err
app.logger.exception("Unhandled error: %s", err)
if request.path.startswith("/api/"):
return (
jsonify(
{
"status": "error",
"code": "internal_error",
# CONSTRAINT: never echo str(err) to the client — it can
# carry internal paths, SQL fragments, or upstream URLs.
# The details live in the server log above.
"message": "internal server error",
}
),
500,
)
raise err