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

  1. Confirm the endpoint is OpenAI-compatible: it must return `{ "data": [{ "embedding": [0.1, ...] }, ...] }`. Inspect a raw response with curl.
  2. If the provider uses a different schema, front it with a thin adapter that reshapes the response into the OpenAI shape.
  3. 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.
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/de968880dbf4948f. Report an issue: GitHub.