From c557ea9a9a2ed0a53f82d74e8f935b77a3feeff4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:19:48 +0800 Subject: [PATCH] fix: report safe M0 publish HTTP status Co-Authored-By: Codex --- .../build_m0_research_publisher_envelope.py | 6 ++++- ...st_build_m0_research_publisher_envelope.py | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/python/scripts/build_m0_research_publisher_envelope.py b/python/scripts/build_m0_research_publisher_envelope.py index 0f87f34..5032d66 100644 --- a/python/scripts/build_m0_research_publisher_envelope.py +++ b/python/scripts/build_m0_research_publisher_envelope.py @@ -340,7 +340,11 @@ def publish_m0_research_publisher_envelope( try: with urllib.request.urlopen(request, timeout=15) as response: # nosec B310 - URL is HTTPS validated above. status = response.getcode() - except (urllib.error.HTTPError, urllib.error.URLError, OSError) as exc: + except urllib.error.HTTPError as exc: + # The status code is sufficient for operational diagnosis and cannot + # disclose a token, request body, or arbitrary response content. + raise M0ResearchPublisherEnvelopeError(f"m0_publish_http_{exc.code}") from exc + except (urllib.error.URLError, OSError) as exc: raise M0ResearchPublisherEnvelopeError("m0_publish_failed") from exc if not isinstance(status, int) or status < 200 or status >= 300: raise M0ResearchPublisherEnvelopeError("m0_publish_failed") diff --git a/python/tests/test_build_m0_research_publisher_envelope.py b/python/tests/test_build_m0_research_publisher_envelope.py index c1b3d56..3e84520 100644 --- a/python/tests/test_build_m0_research_publisher_envelope.py +++ b/python/tests/test_build_m0_research_publisher_envelope.py @@ -7,6 +7,7 @@ import sys import tempfile import unittest +import urllib.error from contextlib import redirect_stdout from pathlib import Path from unittest.mock import patch @@ -283,6 +284,32 @@ def fake_urlopen(request, timeout): }, ) + def test_publish_reports_only_http_status_for_a_rejected_request(self): + envelope = publisher.build_m0_research_publisher_envelope( + source_snapshot=self._snapshot(), + source_artifact=self._artifact("f" * 64), + producer_repository="QuantStrategyLab/QuantRuntimeSettings", + producer_revision="e" * 40, + now="2026-08-21T12:00:00Z", + ) + rejected = urllib.error.HTTPError( + "https://research-console.example/api/internal/sync-m0-research-ledger", + 409, + "Conflict", + hdrs=None, + fp=io.BytesIO(b'{"error":"must-not-be-exposed"}'), + ) + with patch.object(publisher.urllib.request, "urlopen", side_effect=rejected): + with self.assertRaisesRegex(publisher.M0ResearchPublisherEnvelopeError, "m0_publish_http_409") as caught: + publisher.publish_m0_research_publisher_envelope( + envelope, + environ={ + publisher.PUBLISH_URL_ENV: "https://research-console.example/api/internal/sync-m0-research-ledger", + publisher.PUBLISH_TOKEN_ENV: "dedicated-publisher-token", + }, + ) + self.assertNotIn("must-not-be-exposed", str(caught.exception)) + if __name__ == "__main__": unittest.main()