abhigyanpatwari/GitNexus · error · RetryableEmbeddingBodyError

Embedding endpoint returned an unparseable response (${safeU

Error message

Embedding endpoint returned an unparseable response (${safeUrl(url)}, batch ${batchIndex})

What it means

A module-private RetryableEmbeddingBodyError thrown inside the retried fetch callback when the endpoint returns 2xx but the response body cannot be parsed as JSON (e.g. an HTML error page, a captive-portal page, or a truncated stream), provided the parse failure is not itself a terminal network abort (isTerminalNetworkError re-raises untouched). resilientFetch classifies it as retryable-network so the bad body gets the same exponential backoff and circuit-breaker accounting as a 503. It never escapes httpEmbedBatch directly: after retries are exhausted it is converted into the terminal HttpEmbeddingError at error 127.

Source

Thrown at gitnexus/src/core/embeddings/http-client.ts:460

          // and the same breaker accounting as any other endpoint fault (#2790).
          let payload: { data: EmbeddingItem[] };
          try {
            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;

View on GitHub (pinned to d540b00184)

Solutions

  1. Verify the endpoint with curl: `curl -sS -o /dev/null -w '%{content_type}\n' -X POST "$GITNEXUS_EMBEDDING_URL/embeddings" -H "Authorization: Bearer $GITNEXUS_EMBEDDING_API_KEY" -H 'Content-Type: application/json' -d '{"model":"'$GITNEXUS_EMBEDDING_MODEL'","input":"test"}'` — it must be application/json.
  2. Correct GITNEXUS_EMBEDDING_URL to the OpenAI-compatible base (no trailing /embeddings; the client appends it), e.g. https://api.openai.com/v1.
  3. If the truncation is intermittent, raise GITNEXUS_EMBEDDING_HTTP_TIMEOUT_MS and reduce HTTP_BATCH_SIZE pressure by embedding smaller corpora; the retry/backoff should recover transient cases.
  4. Check ingress/proxy buffering and gzip settings on the embedding service.

Example fix

// before: URL points at the marketing site
export GITNEXUS_EMBEDDING_URL=https://provider.example.com

// after: URL points at the OpenAI-compatible API base
export GITNEXUS_EMBEDDING_URL=https://provider.example.com/v1
Defensive patterns

Strategy: retry

Validate before calling

// Probe the endpoint once before the indexing run:
const probe = await fetch(`${process.env.GITNEXUS_EMBEDDING_URL}/embeddings`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.GITNEXUS_EMBEDDING_API_KEY}`,
  },
  body: JSON.stringify({ model: process.env.GITNEXUS_EMBEDDING_MODEL, input: 'probe' }),
});
const ct = probe.headers.get('content-type') ?? '';
if (!ct.includes('application/json')) {
  throw new Error(`Endpoint is not JSON (content-type: ${ct}); check GITNEXUS_EMBEDDING_URL`);
}

Type guard

// RetryableEmbeddingBodyError is module-private; catch its terminal form instead.
import { isHttpEmbeddingError } from 'gitnexus';
const isUnparseableTerminal = (e: unknown): boolean =>
  isHttpEmbeddingError(e) &&
  e instanceof Error &&
  e.message.includes('returned an unparseable response');

Try / catch

// The library already retries inside resilientFetch; at the outer boundary,
// treat a persistent unparseable body as a configuration fault.
try {
  await httpEmbed(texts);
} catch (e) {
  if (isHttpEmbeddingError(e) && e.message.includes('unparseable response')) {
    // surface to operator: endpoint is returning a non-JSON body
  }
  throw e;
}

Prevention

When it happens

Trigger: httpEmbedBatch's fetchImpl: attemptResp.json() rejects and the rejection is not a terminal network error. Concrete cases: baseUrl points at a generic web server returning 200 + HTML; a reverse proxy returns a 200 status JSON-wrap error page; a load balancer truncates the streaming body mid-vector; the endpoint gzips the body and a transparent proxy strips the Content-Encoding header.

Common situations: Wrong GITNEXUS_EMBEDDING_URL pointing at the provider's homepage or console instead of the /v1 endpoint. Corporate captive portal intercepting HTTPS. Misconfigured ingress returning a friendly HTML 200 page. Intermittent upstream truncation under load (the retry loop usually masks this).

Related errors


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