99 lines
2.7 KiB
Python
99 lines
2.7 KiB
Python
"""
|
|
app/api/errors.py
|
|
-----------------
|
|
Consistent JSON error responses for every API endpoint.
|
|
|
|
Every response — success or failure — uses the same envelope:
|
|
|
|
{
|
|
"ok": true | false,
|
|
"data": { ... } | null,
|
|
"error": null | "Human-readable message"
|
|
}
|
|
|
|
Usage
|
|
-----
|
|
from app.api.errors import api_error, api_ok
|
|
|
|
return api_ok({'inspection': {...}})
|
|
return api_error('Inspection not found', 404)
|
|
"""
|
|
|
|
from flask import jsonify
|
|
|
|
|
|
def api_ok(data=None, status=200):
|
|
"""Return a successful JSON response."""
|
|
return jsonify({
|
|
'ok': True,
|
|
'data': data,
|
|
'error': None,
|
|
}), status
|
|
|
|
|
|
def api_error(message: str, status: int = 400, extra: dict | None = None):
|
|
"""Return an error JSON response.
|
|
|
|
`extra` merges additional top-level keys into the envelope — for flags a
|
|
client must branch on rather than parse out of the message, e.g.
|
|
`mfa_required` on a login that needs a second factor. Reserved keys
|
|
(ok/data/error) always win, so a caller cannot accidentally rewrite the
|
|
envelope's shape.
|
|
"""
|
|
payload = {
|
|
'ok': False,
|
|
'data': None,
|
|
'error': message,
|
|
}
|
|
if extra:
|
|
for k, v in extra.items():
|
|
payload.setdefault(k, v)
|
|
return jsonify(payload), status
|
|
|
|
|
|
# ── Registered error handlers (attached to the api blueprint) ─────────────────
|
|
|
|
def register_error_handlers(bp):
|
|
"""
|
|
Attach JSON error handlers to the given blueprint so that Flask
|
|
exceptions (404, 405, 500, etc.) return JSON instead of HTML within
|
|
the /api/v1/ prefix.
|
|
"""
|
|
from werkzeug.exceptions import HTTPException
|
|
|
|
@bp.errorhandler(400)
|
|
def bad_request(e):
|
|
return api_error(str(e.description) if hasattr(e, 'description') else 'Bad request', 400)
|
|
|
|
@bp.errorhandler(401)
|
|
def unauthorized(e):
|
|
return api_error('Authentication required', 401)
|
|
|
|
@bp.errorhandler(403)
|
|
def forbidden(e):
|
|
return api_error('Access denied', 403)
|
|
|
|
@bp.errorhandler(404)
|
|
def not_found(e):
|
|
return api_error('Resource not found', 404)
|
|
|
|
@bp.errorhandler(405)
|
|
def method_not_allowed(e):
|
|
return api_error('Method not allowed', 405)
|
|
|
|
@bp.errorhandler(413)
|
|
def payload_too_large(e):
|
|
return api_error('Uploaded file is too large', 413)
|
|
|
|
@bp.errorhandler(422)
|
|
def unprocessable(e):
|
|
return api_error('Unprocessable request', 422)
|
|
|
|
@bp.errorhandler(500)
|
|
def internal_error(e):
|
|
return api_error('Internal server error', 500)
|
|
|
|
@bp.errorhandler(HTTPException)
|
|
def generic_http(e):
|
|
return api_error(e.description or e.name, e.code)
|