error handling
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
from html import escape
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from flask import jsonify, render_template, request
|
from flask import jsonify, render_template, request
|
||||||
@@ -6,20 +5,21 @@ from jinja2 import TemplateNotFound
|
|||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from .utils import safe_db_rollback
|
from .extensions import db
|
||||||
|
|
||||||
|
|
||||||
JSON_MIMETYPES = ["application/json", "text/html"]
|
def _safe_rollback() -> None:
|
||||||
|
try:
|
||||||
|
db.session.rollback()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _wants_json_response() -> bool:
|
def _wants_json_response() -> bool:
|
||||||
if request.path.startswith("/api/"):
|
if request.path.startswith("/api/"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if request.is_json:
|
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
|
||||||
return True
|
|
||||||
|
|
||||||
best = request.accept_mimetypes.best_match(JSON_MIMETYPES)
|
|
||||||
if not best:
|
if not best:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -30,43 +30,49 @@ def _wants_json_response() -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _status_phrase(status_code: int) -> str:
|
def _get_status_phrase(status_code: int) -> str:
|
||||||
try:
|
try:
|
||||||
return HTTPStatus(status_code).phrase
|
return HTTPStatus(status_code).phrase
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "Blad"
|
return "Blad"
|
||||||
|
|
||||||
|
|
||||||
def _status_description(status_code: int) -> str:
|
def _get_status_description(status_code: int) -> str:
|
||||||
try:
|
try:
|
||||||
return HTTPStatus(status_code).description
|
return HTTPStatus(status_code).description
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return "Wystapil blad podczas przetwarzania zadania."
|
return "Wystapil blad podczas przetwarzania zadania."
|
||||||
|
|
||||||
|
|
||||||
def _plain_fallback(status_code: int, phrase: str, description: str):
|
def _error_headers(status_code: int) -> dict[str, str]:
|
||||||
html = f"""<!doctype html>
|
headers = {}
|
||||||
<html lang=\"pl\">
|
|
||||||
<head>
|
if status_code >= 500:
|
||||||
<meta charset=\"utf-8\">
|
headers.update(
|
||||||
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
|
{
|
||||||
<title>{status_code} {escape(phrase)}</title>
|
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0, private",
|
||||||
</head>
|
"Pragma": "no-cache",
|
||||||
<body>
|
"Expires": "0",
|
||||||
<h1>{status_code} - {escape(phrase)}</h1>
|
"Surrogate-Control": "no-store",
|
||||||
<p>{escape(description)}</p>
|
}
|
||||||
</body>
|
)
|
||||||
</html>"""
|
|
||||||
return html, status_code
|
return headers
|
||||||
|
|
||||||
|
|
||||||
def _render_error(status_code: int, message: str | None = None):
|
def _render_error(status_code: int, message: str | None = None):
|
||||||
phrase = _status_phrase(status_code)
|
phrase = _get_status_phrase(status_code)
|
||||||
description = message or _status_description(status_code)
|
description = message or _get_status_description(status_code)
|
||||||
payload = {"status": status_code, "error": phrase, "message": description}
|
headers = _error_headers(status_code)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"status": status_code,
|
||||||
|
"error": phrase,
|
||||||
|
"message": description,
|
||||||
|
}
|
||||||
|
|
||||||
if _wants_json_response():
|
if _wants_json_response():
|
||||||
return jsonify(payload), status_code
|
return jsonify(payload), status_code, headers
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return (
|
return (
|
||||||
@@ -77,11 +83,14 @@ def _render_error(status_code: int, message: str | None = None):
|
|||||||
error_message=description,
|
error_message=description,
|
||||||
),
|
),
|
||||||
status_code,
|
status_code,
|
||||||
|
headers,
|
||||||
)
|
)
|
||||||
except TemplateNotFound:
|
except TemplateNotFound:
|
||||||
return _plain_fallback(status_code, phrase, description)
|
return (
|
||||||
except Exception:
|
f"{status_code} {phrase}: {description}",
|
||||||
return _plain_fallback(status_code, phrase, description)
|
status_code,
|
||||||
|
headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_error_handlers(app):
|
def register_error_handlers(app):
|
||||||
@@ -91,12 +100,15 @@ def register_error_handlers(app):
|
|||||||
|
|
||||||
@app.errorhandler(OperationalError)
|
@app.errorhandler(OperationalError)
|
||||||
def handle_operational_error(exc):
|
def handle_operational_error(exc):
|
||||||
safe_db_rollback()
|
_safe_rollback()
|
||||||
app.logger.exception("Blad polaczenia z baza danych: %s", exc)
|
app.logger.exception("Blad polaczenia z baza danych: %s", exc)
|
||||||
return _render_error(503, "Baza danych jest chwilowo niedostepna. Sprobuj ponownie za chwile.")
|
return _render_error(
|
||||||
|
503,
|
||||||
|
"Baza danych jest chwilowo niedostepna. Sprobuj ponownie za chwile.",
|
||||||
|
)
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
def handle_unexpected_error(exc):
|
def handle_unexpected_error(exc):
|
||||||
safe_db_rollback()
|
_safe_rollback()
|
||||||
app.logger.exception("Nieobsluzony wyjatek: %s", exc)
|
app.logger.exception("Nieobsluzony wyjatek: %s", exc)
|
||||||
return _render_error(500, "Wystapil nieoczekiwany blad serwera.")
|
return _render_error(500, "Wystapil nieoczekiwany blad serwera.")
|
||||||
@@ -1,21 +1,75 @@
|
|||||||
{% extends "base.html" %}
|
<!doctype html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ error_code }} {{ error_name }}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
{% block title %}{{ error_code }} {{ error_name }}{% endblock %}
|
{% if asset_url is defined %}
|
||||||
|
<link rel="stylesheet" href="{{ asset_url('css/style.css') }}">
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}
|
<style>
|
||||||
<div class="row justify-content-center py-5">
|
body {
|
||||||
<div class="col-12 col-md-10 col-lg-8">
|
margin: 0;
|
||||||
<div class="card border-warning shadow-sm">
|
padding: 0;
|
||||||
<div class="card-body p-4 p-md-5 text-center">
|
}
|
||||||
<div class="display-4 fw-bold mb-3">{{ error_code }}</div>
|
|
||||||
<h1 class="h3 mb-3">{{ error_name }}</h1>
|
.error-page {
|
||||||
<p class="lead mb-4">{{ error_message }}</p>
|
min-height: 100vh;
|
||||||
<div class="d-flex gap-2 justify-content-center flex-wrap">
|
display: flex;
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-primary">Wroc na strone glowna</a>
|
align-items: center;
|
||||||
<a href="javascript:history.back()" class="btn btn-outline-light">Wroc</a>
|
justify-content: center;
|
||||||
</div>
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-actions a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="error-page">
|
||||||
|
<section class="error-box">
|
||||||
|
<div class="error-code">{{ error_code }}</div>
|
||||||
|
<h1 class="error-name">{{ error_name }}</h1>
|
||||||
|
<p class="error-message">{{ error_message }}</p>
|
||||||
|
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/">Powrot na strone glowna</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</body>
|
||||||
{% endblock %}
|
</html>
|
||||||
Reference in New Issue
Block a user