fix(embedding): harden API error handling (PR #1671 review)
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.
This commit is contained in:
parent
d471a9e262
commit
f272c84514
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in New Issue