Repos / pytaku / 3750862e6f
commit 3750862e6f50e8f8a112326063e1db3b337410c6
Author: Bùi Thành Nhân <hi@imnhan.com>
Date: Thu Aug 27 21:39:21 2020 +0700
show source site error to user
New `@handle_source_site_errors` decorator that supports either html or
json responses.
- If html: just render a plain page
- If json: count on frontend code to report error appropriately.
Currently it just `alert()`s the thing.
Need to figure out a better UX.
diff --git a/pyproject.toml b/pyproject.toml
index e6764bf..c95133f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytaku"
-version = "0.3.11"
+version = "0.3.12"
description = "Self-hostable web-based manga reader"
authors = ["Bùi Thành Nhân <hi@imnhan.com>"]
license = "AGPL-3.0-only"
diff --git a/src/mangoapi/base_site.py b/src/mangoapi/base_site.py
index 6f1884d..9bb4dde 100644
--- a/src/mangoapi/base_site.py
+++ b/src/mangoapi/base_site.py
@@ -3,7 +3,11 @@
import requests
-from .exceptions import SourceSite5xxError, SourceSiteUnexpectedError
+from .exceptions import (
+ SourceSite5xxError,
+ SourceSiteTimeoutError,
+ SourceSiteUnexpectedError,
+)
class Site(ABC):
@@ -46,18 +50,21 @@ def title_source_url(self, title_id):
def login(self, username, password):
raise NotImplementedError()
- def _http_request(self, method, *args, **kwargs):
+ def _http_request(self, method, url, *args, **kwargs):
request_func = getattr(self._session, method)
if "timeout" not in kwargs:
kwargs["timeout"] = 5
- resp = request_func(*args, **kwargs)
+ try:
+ resp = request_func(url, *args, **kwargs)
+ except requests.exceptions.Timeout:
+ raise SourceSiteTimeoutError(url)
if 500 <= resp.status_code <= 599:
- raise SourceSite5xxError(resp.text)
+ raise SourceSite5xxError(url, resp.status_code, resp.text)
elif resp.status_code != 200:
- raise SourceSiteUnexpectedError(resp.status_code, resp.text)
+ raise SourceSiteUnexpectedError(url, resp.status_code, resp.text)
return resp
diff --git a/src/mangoapi/exceptions.py b/src/mangoapi/exceptions.py
index a48d331..728d4ab 100644
--- a/src/mangoapi/exceptions.py
+++ b/src/mangoapi/exceptions.py
@@ -1,8 +1,17 @@
-class SourceSite5xxError(Exception):
+class SourceSiteResponseError(Exception):
+ def __init__(self, url, status_code=None, response_text=None):
+ self.url = url
+ self.status_code = status_code
+ self.response_text = response_text
+
+
+class SourceSiteTimeoutError(SourceSiteResponseError):
pass
-class SourceSiteUnexpectedError(Exception):
- def __init__(self, status_code, resp_text):
- self.status_code = status_code
- self.resp_text = resp_text
+class SourceSite5xxError(SourceSiteResponseError):
+ pass
+
+
+class SourceSiteUnexpectedError(SourceSiteResponseError):
+ pass
diff --git a/src/pytaku/decorators.py b/src/pytaku/decorators.py
index 01d1a8d..c7a86c7 100644
--- a/src/pytaku/decorators.py
+++ b/src/pytaku/decorators.py
@@ -1,6 +1,13 @@
from functools import wraps
-from flask import jsonify, request
+from flask import jsonify, render_template, request
+
+from mangoapi.exceptions import (
+ SourceSite5xxError,
+ SourceSiteResponseError,
+ SourceSiteTimeoutError,
+ SourceSiteUnexpectedError,
+)
from .persistence import verify_token
@@ -33,3 +40,40 @@ def decorated_function(*args, **kwargs):
return decorated_function
return decorator
+
+
+SOURCE_SITE_ERRORS = {
+ SourceSiteTimeoutError: {
+ "code": "source_site_timeout",
+ "message": "Source site took too long to respond. Try again later.",
+ },
+ SourceSite5xxError: {
+ "code": "source_site_5xx",
+ "message": "Source site crapped the bed. Try again later.",
+ },
+ SourceSiteUnexpectedError: {
+ "code": "source_site_unexpected",
+ "message": "Unexpected error when requesting source site.",
+ },
+}
+
+
+def handle_source_site_errors(format="json"):
+ assert format in ("json", "html"), f"{format} ain't no format I ever heard of!"
+
+ def outer_func(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except SourceSiteResponseError as err:
+ resp = SOURCE_SITE_ERRORS[err.__class__]
+ resp["detail"] = err.__dict__
+ if format == "json":
+ return jsonify(resp), 500
+ elif format == "html":
+ return render_template("error.html", **resp), 500
+
+ return decorated_function
+
+ return outer_func
diff --git a/src/pytaku/main.py b/src/pytaku/main.py
index 9626226..ec0749e 100644
--- a/src/pytaku/main.py
+++ b/src/pytaku/main.py
@@ -10,7 +10,7 @@
from flask import Flask, jsonify, make_response, render_template, request, url_for
from .conf import config
-from .decorators import process_token
+from .decorators import handle_source_site_errors, process_token
from .persistence import (
create_token,
delete_token,
@@ -209,6 +209,7 @@ def _title(site, title_id, user_id=None):
@app.route("/m/<site>/<title_id>")
+@handle_source_site_errors("html")
def spa_title_view(site, title_id):
title = _title(site, title_id)
return render_template(
@@ -222,6 +223,7 @@ def spa_title_view(site, title_id):
@app.route("/m/<site>/<title_id>/<chapter_id>")
+@handle_source_site_errors("html")
def spa_chapter_view(site, title_id, chapter_id):
chapter = load_chapter(site, title_id, chapter_id)
if not chapter:
@@ -252,6 +254,7 @@ def spa_chapter_view(site, title_id, chapter_id):
@app.route("/api/title/<site>/<title_id>", methods=["GET"])
@process_token(required=False)
+@handle_source_site_errors("json")
def api_title(site, title_id):
title = _title(site, title_id, user_id=request.user_id)
return title
@@ -259,6 +262,7 @@ def api_title(site, title_id):
@app.route("/api/chapter/<site>/<title_id>/<chapter_id>", methods=["GET"])
@process_token(required=False)
+@handle_source_site_errors("json")
def api_chapter(site, title_id, chapter_id):
chapter = load_chapter(site, title_id, chapter_id)
if not chapter:
@@ -382,6 +386,7 @@ def api_search(query):
@app.route("/api/follow", methods=["POST"])
@process_token(required=True)
+@handle_source_site_errors("json")
def api_follow():
should_follow = request.json["follow"]
site = request.json["site"]
@@ -405,12 +410,18 @@ def api_read():
if reads:
for r in reads:
read(
- request.user_id, r["site"], r["title_id"], r["chapter_id"],
+ request.user_id,
+ r["site"],
+ r["title_id"],
+ r["chapter_id"],
)
if unreads:
for u in unreads:
unread(
- request.user_id, u["site"], u["title_id"], u["chapter_id"],
+ request.user_id,
+ u["site"],
+ u["title_id"],
+ u["chapter_id"],
)
# TODO: rewrite read/unread to do bulk updates instead of n+1 queries like these.
# ... Or maybe not. SQLite doesn't mind.
@@ -422,6 +433,7 @@ def api_read():
@app.route("/api/import", methods=["POST"])
@process_token(required=True)
+@handle_source_site_errors("json")
def api_import():
# check if the post request has the file part
if "tachiyomi" not in request.files:
diff --git a/src/pytaku/static/js/models.js b/src/pytaku/static/js/models.js
index e683512..e28f991 100644
--- a/src/pytaku/static/js/models.js
+++ b/src/pytaku/static/js/models.js
@@ -81,6 +81,8 @@ const Auth = {
return m.request(options).catch((err) => {
if (err.code == 401) {
Auth.clearCredentials();
+ } else if (err.code == 500) {
+ alert(err.response.message);
}
throw err;
});
diff --git a/src/pytaku/templates/error.html b/src/pytaku/templates/error.html
new file mode 100644
index 0000000..69da5d8
--- /dev/null
+++ b/src/pytaku/templates/error.html
@@ -0,0 +1,17 @@
+{# vim: ft=htmldjango
+#}
+{% extends "spa.html" %}
+{% block title %}{{ message }}{% endblock %}
+{% block body %}
+<div class="content">
+ <h1>{{ message }}</h1>
+ <p>Error code: {{ code }}</p>
+ <h2>Details:</h2>
+ <ul>
+ {% for key, val in detail.items() %}
+ <li>{{ key }}: {{ val }}</li>
+ {% endfor %}
+ </ul>
+
+</div>
+{% endblock %}
diff --git a/src/pytaku/templates/spa.html b/src/pytaku/templates/spa.html
index dabbf90..cc61dc2 100644
--- a/src/pytaku/templates/spa.html
+++ b/src/pytaku/templates/spa.html
@@ -24,6 +24,8 @@
</head>
<body>
+ {% block body %}
+
<div id="spa-root">
<noscript>
<p>Please enable JavaScript to use Pytaku.</p>
@@ -45,5 +47,7 @@
<script>const initialState = "{{ initial_state }}";</script>
<script src="{{ url_for('static', filename='vendored/mithril.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}" type="module"></script>
+
+ {% endblock %}
</body>
</html>