From 1ffc3cff9d3a2192ddaa7591fefba47ec585e3d1 Mon Sep 17 00:00:00 2001 From: oztturk Date: Wed, 26 Aug 2026 16:29:41 +0300 Subject: [PATCH] Let an error body that is an array still be read Google answers some failures with a JSON array holding the object every other provider sends on its own. _extract_error called .get() on it and raised AttributeError, which is not the ApiError every caller is holding, so a 503 from Google took the whole dictation down instead of pasting the raw transcript with the failure shown beside it. Found by dictating against a Google AI Studio outage: HTTP Error 503: Service Unavailable AttributeError: 'list' object has no attribute 'get' It runs while an exception is being raised, so it now ends in a string whatever arrives. Co-Authored-By: Claude Opus 5 --- dikte/api.py | 13 +++++++++++++ tests/test_api.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/dikte/api.py b/dikte/api.py index 16c85c8..111099d 100644 --- a/dikte/api.py +++ b/dikte/api.py @@ -262,10 +262,23 @@ def _request(url, data, headers, timeout=120, aborter=None): def _extract_error(body): + """The line worth showing out of a failed request's body. + + Whatever comes back, this has to end in a string: it is called while an + ApiError is being raised, and an exception thrown here would escape the + `except ApiError` every caller is holding and lose the dictation the raw + transcript would otherwise have been pasted from. + """ try: payload = json.loads(body) except json.JSONDecodeError: return body[:300] + if isinstance(payload, list): + # Google answers some failures with an array holding the object the + # other providers send on its own. + payload = next((item for item in payload if isinstance(item, dict)), None) + if not isinstance(payload, dict): + return body[:300] err = payload.get("error") if isinstance(err, dict): return err.get("message") or json.dumps(err)[:300] diff --git a/tests/test_api.py b/tests/test_api.py index 09b49e1..ccaa527 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -119,9 +119,22 @@ class ExtractError(unittest.TestCase): body = json.dumps({"error": {"code": 42}}) self.assertIn("42", api._extract_error(body)) + def test_an_error_wrapped_in_an_array(self): + """Google's 503 arrives this way, and .get() on a list raises.""" + body = json.dumps([{"error": {"code": 503, + "message": "The model is overloaded."}}]) + self.assertEqual(api._extract_error(body), "The model is overloaded.") + def test_a_body_that_is_not_json(self): self.assertEqual(api._extract_error("502"), "502") + def test_no_shape_at_all_still_comes_back_as_a_string(self): + """It runs while an ApiError is being raised: throwing here would + escape the `except ApiError` holding the raw transcript.""" + for body in ("[]", "[1, 2]", '"a string"', "null", "17"): + with self.subTest(body=body): + self.assertIsInstance(api._extract_error(body), str) + def test_a_wall_of_html_is_cut_short(self): self.assertEqual(len(api._extract_error("x" * 5000)), 300)