From 3ba7eaefd08f008846e9ffa74310104cafe66393 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Thu, 14 May 2026 10:15:19 +0100 Subject: [PATCH] Show full Wikipedia 429 error message --- add_links/api.py | 2 +- test_api.py | 30 ++++++++++++++++++++++++++++++ web_view.py | 17 ++++++++++++++--- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 test_api.py diff --git a/add_links/api.py b/add_links/api.py index 6f57def..0742fe2 100644 --- a/add_links/api.py +++ b/add_links/api.py @@ -98,7 +98,7 @@ def api_get(params: StrDict) -> StrDict: if webpage_error in r.text: raise MediawikiError(webpage_error) if r.status_code == 429: - raise MediawikiError("Wikipedia rate limit exceeded — wait a moment and try again.") + raise MediawikiError(r.text) raise MediawikiError(f"HTTP {r.status_code}: {r.text[:200]!r}") check_for_error(ret) return ret diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..14968ca --- /dev/null +++ b/test_api.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import Mock, patch + +from simplejson.scanner import JSONDecodeError + +from add_links import api + + +class ApiGetTests(unittest.TestCase): + def test_429_error_preserves_full_message(self) -> None: + response = Mock() + response.status_code = 429 + response.text = ( + "Too many requests. If you are a tool operator, contact " + "noc@example.org for help." + ) + response.json.side_effect = JSONDecodeError("bad json", "", 0) + + session = Mock() + session.get.return_value = response + + with patch("add_links.api._get_active_session", return_value=session): + with self.assertRaises(api.MediawikiError) as ctx: + api.api_get({"action": "query"}) + + self.assertEqual(str(ctx.exception), response.text) + + +if __name__ == "__main__": + unittest.main() diff --git a/web_view.py b/web_view.py index 7895c8d..6f7c9c5 100755 --- a/web_view.py +++ b/web_view.py @@ -37,6 +37,17 @@ class Hit(typing.TypedDict): timestamp: str +def render_error(message: str) -> str: + """Render shared error page.""" + return flask.render_template("error.html", message=message) + + +def render_mediawiki_error(error: Exception, *, prefix: str | None = None) -> str: + """Render MediaWiki errors.""" + message = f"{prefix}: {error}" if prefix else str(error) + return render_error(message) + + def load_examples() -> list[dict[str, str | int]]: """Load examples.""" return [json.loads(line) for line in open("examples")] @@ -203,7 +214,7 @@ def start_oauth() -> Response: fetch_response = oauth.fetch_request_token(request_token_url) except TokenRequestDenied as e: return flask.make_response( - flask.render_template("error.html", message=str(e)), 502 + render_error(str(e)), 502 ) flask.session["owner_key"] = fetch_response.get("oauth_token") @@ -325,7 +336,7 @@ def handle_post(url_title: str) -> Response: return flask.redirect(flask.url_for("start_oauth", next=next_url)) except (mediawiki_api.APIError, api.MediawikiError) as e: return flask.make_response( - flask.render_template("error.html", message=f"Save failed: {e}"), 502 + render_mediawiki_error(e, prefix="Save failed"), 502 ) flask.session["saves"] = flask.session.get("saves", 0) + 1 saves_by_title: dict[str, int] = flask.session.get("saves_by_title", {}) @@ -357,7 +368,7 @@ def article_page(url_title: str) -> str | Response: _no_link_count, hits = search_no_link(from_title, redirect_to) except api.MediawikiError as e: return flask.make_response( - flask.render_template("error.html", message=str(e)), 502 + render_mediawiki_error(e), 502 ) # Filter out candidates already processed this session