From e1b189ad3c29c45d69921989b86f9c97fc931f01 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Mon, 11 May 2026 23:08:41 +0200 Subject: [PATCH] workshop_routes: narrow refresh's steam exception handler Catch only requests.RequestException in refresh_overlay so that server-side data errors (e.g., ValueError) bubble up as 500 rather than being disguised as a 502 "steam api error". Update the 502 test to use a real requests exception, add a sibling test that verifies non-requests exceptions propagate, and explicitly assert that refresh enqueues a build_overlay job even when Steam returns no entries. Co-Authored-By: Claude Sonnet 4.6 --- l4d2web/routes/workshop_routes.py | 3 ++- l4d2web/tests/test_workshop_routes.py | 32 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/l4d2web/routes/workshop_routes.py b/l4d2web/routes/workshop_routes.py index 7a318ff..249d42c 100644 --- a/l4d2web/routes/workshop_routes.py +++ b/l4d2web/routes/workshop_routes.py @@ -2,6 +2,7 @@ admin global refresh).""" from __future__ import annotations +import requests from flask import Blueprint, Response, redirect, request from sqlalchemy import delete as sa_delete from sqlalchemy import select @@ -145,7 +146,7 @@ def refresh_overlay(overlay_id: int) -> Response: try: metas = steam_workshop.fetch_metadata_batch(steam_ids, mode="refresh") - except Exception as exc: + except requests.RequestException as exc: return Response(f"steam api error: {exc}", status=502) metas_by_id = {m.steam_id: m for m in metas} diff --git a/l4d2web/tests/test_workshop_routes.py b/l4d2web/tests/test_workshop_routes.py index 99b2198..6ec5b35 100644 --- a/l4d2web/tests/test_workshop_routes.py +++ b/l4d2web/tests/test_workshop_routes.py @@ -385,7 +385,8 @@ def test_overlay_refresh_502_on_steam_error(overlay_for): operation="build_overlay", overlay_id=overlay_id, state="queued" ).count() - with patch.object(steam_workshop, "fetch_metadata_batch", side_effect=Exception("boom")): + import requests as _requests + with patch.object(steam_workshop, "fetch_metadata_batch", side_effect=_requests.ConnectionError("boom")): response = user_client.post( f"/overlays/{overlay_id}/refresh", headers={"X-CSRF-Token": "test-token"}, @@ -400,6 +401,31 @@ def test_overlay_refresh_502_on_steam_error(overlay_for): assert n == baseline_job_count +def test_overlay_refresh_non_requests_exception_propagates(overlay_for): + """A non-requests exception (e.g., ValueError from a server-side data + issue) must not be disguised as a 502 — let it bubble up as 500.""" + app, login, user_id, _admin_id, overlay_id = overlay_for + user_client = login(user_id) + with _patch_steam([_meta("1001")]): + user_client.post( + f"/overlays/{overlay_id}/items", + data={"input": "1001", "input_mode": "items"}, + headers={"X-CSRF-Token": "test-token"}, + ) + with session_scope() as session: + for job in session.query(Job).filter_by(operation="build_overlay", state="queued"): + job.state = "succeeded" + + # Flask in TESTING mode re-raises uncaught exceptions instead of returning + # 500, so we assert the ValueError propagates rather than checking a status. + with patch.object(steam_workshop, "fetch_metadata_batch", side_effect=ValueError("bad steam_id in db")): + with pytest.raises(ValueError, match="bad steam_id in db"): + user_client.post( + f"/overlays/{overlay_id}/refresh", + headers={"X-CSRF-Token": "test-token"}, + ) + + def test_overlay_refresh_missing_item_records_last_error(overlay_for): app, login, user_id, _admin_id, overlay_id = overlay_for user_client = login(user_id) @@ -422,3 +448,7 @@ def test_overlay_refresh_missing_item_records_last_error(overlay_for): with session_scope() as session: wi = session.query(WorkshopItem).filter_by(steam_id="1001").one() assert "no entry" in wi.last_error + new_jobs = session.query(Job).filter_by( + operation="build_overlay", overlay_id=overlay_id, state="queued" + ).all() + assert len(new_jobs) == 1, "refresh should enqueue build even when Steam returns no entries"