abhigyanpatwari/GitNexus · error · RetryableEmbeddingBodyError
Embedding endpoint returned an unexpected response shape (${
Error message
Embedding endpoint returned an unexpected response shape (${safeUrl(url)}, batch ${batchIndex}) What it means
A module-private RetryableEmbeddingBodyError thrown inside the retried fetch callback when the body parses as JSON but is not the expected `{ data: EmbeddingItem[] }` shape — specifically when `payload.data` is not an array, or not every element is an `{ embedding: number[] }` item (isEmbeddingItem). Like error 123 it is retried and counted against the circuit breaker, then surfaced terminally as HttpEmbeddingError (127) on exhaustion.
Source
Thrown at gitnexus/src/core/embeddings/http-client.ts:463
payload = (await attemptResp.json()) as { data: EmbeddingItem[] };
} catch (err) {
// Not every `.json()` rejection is a parse error: the per-attempt
// signal (`AbortSignal.any([caller, AbortSignal.timeout(...)])`) is
// wired to the body stream, so a stalled body rejects with the abort
// reason. Re-raise those untouched — `isTerminalNetworkError` is
// `resilientFetch`'s own predicate, so this test agrees with
// `classifyOutcome` by construction. Wrapping one would flip its
// verdict from `terminal-network` (returned without retry AND
// without touching the breaker, via `recordNeutral()`) to
// `retryable-network` (retried, then `breaker.recordFailure()`): the
// same timeout would take 3 attempts instead of 1, count toward the
// process-global `embeddings-http` breaker, and reach the operator as
// "unparseable response" so they never reach for the timeout knob.
if (isTerminalNetworkError(err)) throw err;
throw new RetryableEmbeddingBodyError(unparseableMessage(), { cause: err });
}
if (!Array.isArray(payload?.data) || !payload.data.every(isEmbeddingItem)) {
throw new RetryableEmbeddingBodyError(unexpectedShapeMessage());
}
// Cardinality belongs *inside* the retry loop. `every(isEmbeddingItem)`
// is vacuously true for `[]` and true for any array shorter than the
// request, so a 200 carrying `{"data": []}` — or half the vectors —
// used to be classified `success`, call `breaker.recordSuccess()`
// (erasing the outage signal), and only then fail terminally after a
// single attempt. A short body is a truncated body: same backoff, same
// breaker accounting as any other endpoint fault (#2790).
if (payload.data.length !== batch.length) {
throw new RetryableEmbeddingBodyError(
countMismatchMessage(payload.data.length, batch.length, safeUrl(url), batchIndex),
);
}
parsed = payload.data;
return attemptResp;
},
breakerKey: HTTP_BREAKER_KEY,
retry: {View on GitHub (pinned to d540b00184)
Solutions
- Confirm the endpoint is OpenAI-compatible: it must return `{ "data": [{ "embedding": [0.1, ...] }, ...] }`. Inspect a raw response with curl.
- If the provider uses a different schema, front it with a thin adapter that reshapes the response into the OpenAI shape.
- If the body occasionally contains error envelopes, fix the upstream gateway to use proper non-2xx status codes; the retry loop tolerates transient corruption but persistent 200-errors will exhaust it.
- Check that the model name in GITNEXUS_EMBEDDING_MODEL is valid for the endpoint — some providers return a 200 JSON error for an unknown model.
Example fix
// before: provider returns { "vectors": [...] }
// (no env fix; requires an adapter in front of the endpoint)
// after: endpoint returns OpenAI shape
// { "data": [{ "embedding": [0.1, 0.2, ...] }] } Defensive patterns
Strategy: validation
Validate before calling
// Verify the OpenAI shape on a probe response:
const r = await fetch(`${URL}/embeddings`, { /* ... */ });
const body = await r.json();
const ok =
Array.isArray(body?.data) &&
body.data.every((it: unknown) =>
it !== null && typeof it === 'object' &&
Array.isArray((it as any).embedding) &&
(it as any).embedding.every((n: unknown) => typeof n === 'number'));
if (!ok) throw new Error('Endpoint response is not OpenAI-shaped { data: [{ embedding: number[] }] }'); Type guard
import { isHttpEmbeddingError } from 'gitnexus';
const isUnexpectedShape = (e: unknown): boolean =>
isHttpEmbeddingError(e) &&
e instanceof Error &&
e.message.includes('unexpected response shape'); Try / catch
try {
await httpEmbed(texts);
} catch (e) {
if (isHttpEmbeddingError(e) && e.message.includes('unexpected response shape')) {
// provider schema is not OpenAI-compatible; front with an adapter
}
throw e;
} Prevention
- Use an OpenAI-compatible endpoint or wrap a non-standard provider in a reshaping adapter.
- Reject providers that return JSON error envelopes with HTTP 200.
- Probe once at startup with the isEmbeddingItem-shaped validator above.
When it happens
Trigger: httpEmbedBatch's fetchImpl after a successful .json(): the parsed object has no `data` array (e.g. `{ "error": "..." }` returned with HTTP 200), `data` is an object instead of an array, or `data` contains items shaped wrong (e.g. `{ "embedding": "0.1,0.2" }` string instead of number array, or `{ "vector": [...] }` with the wrong key, or `null` entries).
Common situations: Endpoint returns a JSON error envelope with HTTP 200 (some proxies/gateways do this). Provider uses a non-OpenAI response schema (e.g. Cohere's `{ embeddings: [...] }` or a custom `{ vectors: [...] }`). Provider returns embedding as a base64 string or comma-separated string instead of a JSON number array. A `null` sneaks into the data array.
Related errors
- Embedding endpoint returned an unparseable response (${safeU
- Embedding endpoint returned ${received} vectors for ${expect
- ${err.terminalMessage}
- Embedding endpoint circuit open (${safeUrl(url)}, batch ${ba
- Embedding request timed out after ${timeoutMs}ms (${safeUrl(
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/de968880dbf4948f.
Report an issue: GitHub.