From 86569ba8c25580a63b624681b260675b40a5db51 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 6 Nov 2026 21:15:35 +0800 Subject: [PATCH] fix: Categorize paper IAM HTTP failures safely --- scripts/paper_scheduler_get_iam.py | 13 ++++++- tests/test_paper_scheduler_get_iam.py | 51 ++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/scripts/paper_scheduler_get_iam.py b/scripts/paper_scheduler_get_iam.py index 87c116b..75e7793 100644 --- a/scripts/paper_scheduler_get_iam.py +++ b/scripts/paper_scheduler_get_iam.py @@ -183,7 +183,15 @@ def redirect_request(self, req, fp, code, msg, headers, newurl): except urllib.error.HTTPError as error: if missing and method == "GET" and error.code == 404: return None - raise Blocked("remote_rejected_or_unknown_stop_no_retry", phase=phase) from None + # Fixed categories only: never render any HTTPError payload. + category = { + 401: "remote_http_401_stop_no_retry", + 403: "remote_http_403_stop_no_retry", + 429: "remote_http_429_stop_no_retry", + }.get(error.code, "remote_http_other_stop_no_retry") + if 500 <= error.code <= 599: + category = "remote_http_5xx_stop_no_retry" + raise Blocked(category, phase=phase) from None except Exception: raise Blocked("remote_result_unknown_stop_no_retry", phase=phase) from None @@ -201,6 +209,9 @@ def main(): "binding_collision", "orphan_binding_collision", "concurrent_drift", "after_readback_drift", "authentication_failed", "remote_rejected_or_unknown_stop_no_retry", "remote_result_unknown_stop_no_retry", + "remote_http_401_stop_no_retry", "remote_http_403_stop_no_retry", + "remote_http_429_stop_no_retry", "remote_http_5xx_stop_no_retry", + "remote_http_other_stop_no_retry", } category = error.args[0] if len(error.args) == 1 else None if type(category) is not str or category not in categories: diff --git a/tests/test_paper_scheduler_get_iam.py b/tests/test_paper_scheduler_get_iam.py index 087aa55..39067b0 100644 --- a/tests/test_paper_scheduler_get_iam.py +++ b/tests/test_paper_scheduler_get_iam.py @@ -265,7 +265,7 @@ def open(self, req, timeout): raise TimeoutError(HOSTILE) monkeypatch.setattr(m.urllib.request, "build_opener", lambda handler: Opener()) assert m.main() == 1 - category = "remote_rejected_or_unknown_stop_no_retry" if failure == "http" else "remote_result_unknown_stop_no_retry" + category = "remote_http_403_stop_no_retry" if failure == "http" else "remote_result_unknown_stop_no_retry" assert capsys.readouterr() == (f"BLOCKED: {category} phase={phase}\n", "") assert len(calls) == (1 if phase == "role_get" else 2) assert all(req.get_method() == "GET" or req.full_url.endswith(":getIamPolicy") for req in calls) @@ -280,3 +280,52 @@ def fail(): monkeypatch.setattr(m, "Cloud", fail) assert m.main() == 1 assert capsys.readouterr() == ("BLOCKED: authentication_failed phase=unknown\n", "") + + +@pytest.mark.parametrize("phase", ["role_get", "get_iam_policy", "unknown"]) +@pytest.mark.parametrize("status,category", [ + (401, "remote_http_401_stop_no_retry"), + (403, "remote_http_403_stop_no_retry"), + (429, "remote_http_429_stop_no_retry"), + (500, "remote_http_5xx_stop_no_retry"), + (503, "remote_http_5xx_stop_no_retry"), + (599, "remote_http_5xx_stop_no_retry"), + (400, "remote_http_other_stop_no_retry"), + (404, "remote_http_other_stop_no_retry"), + (302, "remote_http_other_stop_no_retry"), + (600, "remote_http_other_stop_no_retry"), +]) +def test_http_status_category_is_safe_and_stops(status, category, phase, monkeypatch, capsys): + from io import BytesIO + monkeypatch.setattr(m.os, "environ", ENV) + monkeypatch.setattr(m.subprocess, "run", lambda *a, **k: type("Result", (), {"stdout": b"fake-token"})()) + calls = [] + method = "GET" if phase == "role_get" else "POST" + url = "https://fake.invalid/role" + if phase == "get_iam_policy": + url += ":getIamPolicy" + class Opener: + def open(self, req, timeout): + calls.append(req) + raise m.urllib.error.HTTPError(url, status, HOSTILE, {"X-Fake": HOSTILE}, BytesIO(HOSTILE.encode())) + monkeypatch.setattr(m.urllib.request, "build_opener", lambda handler: Opener()) + def execute(env, request): + request(method, url) + pytest.fail("HTTP failure must stop execution") + monkeypatch.setattr(m, "execute", execute) + assert m.main() == 1 + assert capsys.readouterr() == (f"BLOCKED: {category} phase={phase}\n", "") + assert len(calls) == 1 + + +def test_role_get_404_remains_accepted_without_output(monkeypatch, capsys): + calls = [] + monkeypatch.setattr(m.subprocess, "run", lambda *a, **k: type("Result", (), {"stdout": b"fake-token"})()) + class Opener: + def open(self, req, timeout): + calls.append(req) + raise m.urllib.error.HTTPError(req.full_url, 404, HOSTILE, {}, None) + monkeypatch.setattr(m.urllib.request, "build_opener", lambda handler: Opener()) + assert m.Cloud()("GET", "https://fake.invalid/role", missing=True) is None + assert len(calls) == 1 + assert capsys.readouterr() == ("", "")