From deebdc8835c3ce47dd2734bebcd099f7f75db595 Mon Sep 17 00:00:00 2001 From: Justin Merrell Date: Tue, 31 Mar 2026 21:36:30 +0000 Subject: [PATCH] fix: use API detail message in BundleNotFoundError instead of raw URL When the API returns a 404 with an RFC 9457 Problem Details body, the SDK now extracts and surfaces the `detail` field (e.g., "Bundle version with identifier '1.0.0' not found") instead of showing the raw request URL. Falls back to URL when no JSON body is present. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/musher/_http.py | 7 ++++++- tests/test_http.py | 16 ++++++++++++++++ uv.lock | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/musher/_http.py b/src/musher/_http.py index c64c3a1..8c79d7f 100644 --- a/src/musher/_http.py +++ b/src/musher/_http.py @@ -89,7 +89,12 @@ def _raise_for_status(response: httpx.Response) -> None: raise AuthenticationError("Invalid or missing API token") if status == 404: # noqa: PLR2004 - raise BundleNotFoundError(str(response.url)) + try: + body_404: dict[str, object] = response.json() # pyright: ignore[reportAny] + detail_404 = str(body_404.get("detail", "")) + except (ValueError, KeyError): + detail_404 = "" + raise BundleNotFoundError(detail_404 or str(response.url)) if status == 429: # noqa: PLR2004 retry_after_header: str | None = response.headers.get("Retry-After") # pyright: ignore[reportAny] diff --git a/tests/test_http.py b/tests/test_http.py index 66833ce..9bce7e8 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -64,6 +64,22 @@ async def test_404_raises_bundle_not_found(self, transport: HTTPTransport): with pytest.raises(BundleNotFoundError): await transport.get("/v1/test") + @respx.mock + async def test_404_with_json_body_uses_detail(self, transport: HTTPTransport): + respx.get("https://api.test.dev/v1/test").mock( + return_value=httpx.Response( + 404, + json={ + "type": "https://api.platform.musher.dev/errors/not-found", + "title": "Resource Not Found", + "status": 404, + "detail": "Bundle version with identifier '1.0.0' not found", + }, + ) + ) + with pytest.raises(BundleNotFoundError, match="Bundle version with identifier"): + await transport.get("/v1/test") + @respx.mock async def test_429_raises_rate_limit_with_retry_after(self, transport: HTTPTransport): respx.get("https://api.test.dev/v1/test").mock( diff --git a/uv.lock b/uv.lock index cde3b64..78397be 100644 --- a/uv.lock +++ b/uv.lock @@ -1439,7 +1439,7 @@ wheels = [ [[package]] name = "musher-sdk" -version = "0.3.2" +version = "0.3.3" source = { editable = "." } dependencies = [ { name = "httpx" },