From f272c84514be3c0c44c64017a9fa8b1faca82b13 Mon Sep 17 00:00:00 2001 From: maximilize <3752128+maximilize@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:26:49 +0200 Subject: [PATCH] fix(embedding): harden API error handling (PR #1671 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the gemini-code-assist review on #1671: - Wrap `http.client.HTTPException` (BadStatusLine / IncompleteRead — common with local/overloaded servers) and `ValueError` (invalid/missing URL scheme; also subsumes `json.JSONDecodeError`) in `EmbeddingAPIError` instead of letting them crash the caller. - Reject a non-dict top-level JSON response before calling `.get()` on it, so a JSON list/`null`/string yields a clear `EmbeddingAPIError` rather than an unhandled `AttributeError`. - Add tests for all three cases. --- mempalace/embedding.py | 10 +++++++++- tests/test_embedding_api.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/mempalace/embedding.py b/mempalace/embedding.py index 76c1179..7d4eb5c 100644 --- a/mempalace/embedding.py +++ b/mempalace/embedding.py @@ -526,6 +526,7 @@ class OpenAICompatEmbeddingFunction: return self(input) def __call__(self, input): # noqa: A002 — ChromaDB EF protocol uses `input` + import http.client import json from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen @@ -550,7 +551,10 @@ class OpenAICompatEmbeddingFunction: try: with urlopen(req, timeout=_EF_API_TIMEOUT) as resp: data = json.loads(resp.read()) - except (HTTPError, URLError, OSError, json.JSONDecodeError) as e: + # ValueError covers an invalid/missing URL scheme and json.JSONDecodeError; + # http.client.HTTPException covers low-level protocol faults (BadStatusLine, + # IncompleteRead) common with local/overloaded servers. + except (HTTPError, URLError, OSError, http.client.HTTPException, ValueError) as e: raise EmbeddingAPIError( f"Embedding API request to {self._url} failed: {e}. Check that the " f"server is reachable and MEMPALACE_EMBEDDING_API_URL / embedding_api_url " @@ -572,6 +576,10 @@ class OpenAICompatEmbeddingFunction: """ import numpy as np + if not isinstance(data, dict): + raise EmbeddingAPIError( + f"Embedding API at {self._url} returned a non-object response: {data}" + ) rows = data.get("data") if not isinstance(rows, list): raise EmbeddingAPIError( diff --git a/tests/test_embedding_api.py b/tests/test_embedding_api.py index 686cfa2..0f07b30 100644 --- a/tests/test_embedding_api.py +++ b/tests/test_embedding_api.py @@ -285,6 +285,40 @@ def test_raises_on_non_contiguous_indices(monkeypatch): ef(["a", "b"]) +def test_raises_on_http_protocol_exception(monkeypatch): + # BadStatusLine / IncompleteRead — common with local/overloaded servers. + from http.client import HTTPException + + def boom(req, timeout=None): + raise HTTPException("incomplete read") + + monkeypatch.setattr("urllib.request.urlopen", boom) + ef = embedding.OpenAICompatEmbeddingFunction("http://h", "m") + with pytest.raises(embedding.EmbeddingAPIError, match="failed"): + ef(["a"]) + + +def test_raises_on_value_error_from_urlopen(monkeypatch): + # urlopen raises ValueError on an invalid/missing URL scheme. + def boom(req, timeout=None): + raise ValueError("unknown url type") + + monkeypatch.setattr("urllib.request.urlopen", boom) + ef = embedding.OpenAICompatEmbeddingFunction("http://h", "m") + with pytest.raises(embedding.EmbeddingAPIError, match="failed"): + ef(["a"]) + + +def test_raises_on_non_object_response(monkeypatch): + def bad(req, timeout=None): + return _FakeResp(json.dumps([1, 2, 3]).encode()) # JSON list, not an object + + monkeypatch.setattr("urllib.request.urlopen", bad) + ef = embedding.OpenAICompatEmbeddingFunction("http://h", "m") + with pytest.raises(embedding.EmbeddingAPIError, match="non-object response"): + ef(["a"]) + + def test_raises_and_surfaces_server_error_body(monkeypatch): # HTTP 200 with an OpenAI-style error envelope (no "data") — surface it. def err(req, timeout=None):