From 19b7bbaedec5c104fcfea406556e767c6efa93c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 10:01:52 +0900 Subject: [PATCH] fix(ci): close HTTP error response bodies Signed-off-by: Seongho Bae --- scripts/ci/noema_review_gate.py | 5 ++- scripts/ci/pingora_edge_policy.py | 2 ++ scripts/ci/reconcile_repository_metadata.py | 4 ++- scripts/ci/sandboxed_web_e2e.py | 4 ++- tests/test_noema_review_gate.py | 5 ++- tests/test_pingora_edge_policy.py | 2 ++ ...t_repository_metadata_live_verification.py | 22 +++++++++++++ tests/test_sandboxed_web_e2e.py | 31 +++++++++++++++++++ 8 files changed, 71 insertions(+), 4 deletions(-) diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 5ab7e830f3..4d2f7b7c38 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -1636,7 +1636,10 @@ def call_llm( gateway_telemetry: dict[str, str | int] = {} if isinstance(exc, urllib.error.HTTPError): active_phase = "response_error" - gateway_telemetry = _extract_http_error_telemetry(exc) + try: + gateway_telemetry = _extract_http_error_telemetry(exc) + finally: + exc.close() model_value = gateway_telemetry.get("served_model") served_model = model_value if isinstance(model_value, str) else None elapsed = time.monotonic() - attempt_started diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 33e58ed876..8a8aa524e5 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -304,6 +304,8 @@ def _github_open_json(url: str, token: str) -> object: with github_opener.open(request, timeout=30) as response: payload = response.read(MAX_RESPONSE_BYTES + 1) except (HTTPError, URLError, TimeoutError) as exc: + if isinstance(exc, HTTPError): + exc.close() raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc if len(payload) > MAX_RESPONSE_BYTES: raise PolicyError("GitHub API policy response exceeded the bounded response size") diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 36a910ffa8..988d77d273 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -16,7 +16,7 @@ import sys from pathlib import Path from typing import Any -from urllib.error import URLError +from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener @@ -247,6 +247,8 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: if not response.read(1): raise RuntimeError(f"GitHub Pages returned empty content for {repository}") except (URLError, TimeoutError, OSError) as exc: + if isinstance(exc, HTTPError): + exc.close() raise RuntimeError(f"GitHub Pages is not reachable for {repository}") from exc diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index b0376c0822..25859af3c6 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -584,7 +584,9 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: if 200 <= response.status < 500: return True time.sleep(1) - except (urllib.error.URLError, TimeoutError): + except (urllib.error.URLError, TimeoutError) as exc: + if isinstance(exc, urllib.error.HTTPError): + exc.close() time.sleep(1) return False diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 6422ae5012..f05c12e989 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1613,10 +1613,12 @@ def test_call_llm_reports_only_safe_model_from_bounded_http_error(monkeypatch, c } ).encode() + response_body = io.BytesIO(body) + class Opener: def open(self, request): raise noema.urllib.error.HTTPError( - request.full_url, 502, "Bad Gateway", {}, io.BytesIO(body) + request.full_url, 502, "Bad Gateway", {}, response_body ) monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) @@ -1637,6 +1639,7 @@ def open(self, request): assert "terminal_reason=eligible_candidates_exhausted" in output assert secret not in output assert secret not in diagnostic + assert response_body.closed @pytest.mark.parametrize( diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index c5d4e9d7a3..61481ed3e3 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -792,6 +792,8 @@ def fail(_request: object, timeout: int) -> object: monkeypatch.setattr(policy.github_opener, "open", fail) with pytest.raises(policy.PolicyError, match=type(exc).__name__): policy._github_open_json("https://api.github.com/repos/a/b", "token") + if isinstance(exc, HTTPError): + assert exc.fp.closed def test_github_open_json_rejects_oversized_and_malformed_payloads(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/test_repository_metadata_live_verification.py b/tests/test_repository_metadata_live_verification.py index 6ae83d8c47..958abae23f 100644 --- a/tests/test_repository_metadata_live_verification.py +++ b/tests/test_repository_metadata_live_verification.py @@ -5,6 +5,7 @@ import argparse import importlib.util import json +from io import BytesIO from pathlib import Path import pytest @@ -63,6 +64,27 @@ def open(self, request, timeout): return self.response +def test_pages_transport_error_closes_response_body(monkeypatch) -> None: + body = BytesIO(b"redirect") + error = RECONCILER.HTTPError( + "https://contextualwisdomlab.github.io/Repo/", 302, "redirect", {}, body + ) + monkeypatch.setattr( + RECONCILER, "build_opener", lambda *_args: FakeOpener(error=error) + ) + + with pytest.raises(RuntimeError, match="not reachable"): + RECONCILER._pages_publication_ready( + "Repo", + { + "status": "built", + "html_url": "https://contextualwisdomlab.github.io/Repo/", + }, + ) + + assert body.closed + + def install_live_state( monkeypatch, *, diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..1569d62c8c 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -6,6 +6,7 @@ import socket import subprocess import sys +from io import BytesIO from pathlib import Path import pytest @@ -695,6 +696,36 @@ def open(self, url, timeout): assert sandboxed_web_e2e.wait_for_url("http://127.0.0.1:8000/health", 1, service) is False +def test_wait_for_url_closes_http_error_response(monkeypatch, tmp_path): + class RunningProcess: + def poll(self): + return None + + body = BytesIO(b"redirect") + error = sandboxed_web_e2e.urllib.error.HTTPError( + "http://127.0.0.1:8000/health", 302, "Found", {}, body + ) + ticks = iter([0, 0, 2]) + monkeypatch.setattr(sandboxed_web_e2e.time, "monotonic", lambda: next(ticks)) + monkeypatch.setattr(sandboxed_web_e2e.time, "sleep", lambda _seconds: None) + + class FailingOpener: + def open(self, _url, timeout): + raise error + + monkeypatch.setattr( + sandboxed_web_e2e.urllib.request, "build_opener", lambda *_args: FailingOpener() + ) + service = sandboxed_web_e2e.Service( + "web", "serve", RunningProcess(), tmp_path / "web.log" + ) + + assert not sandboxed_web_e2e.wait_for_url( + "http://127.0.0.1:8000/health", 1, service + ) + assert body.closed + + def test_main_runs_with_stubbed_services(monkeypatch, tmp_path, capsys): """Main records success evidence without requiring real POSIX services.""" repo = tmp_path / "repo"