abhigyanpatwari/GitNexus · error · RetryableEmbeddingBodyError
Embedding endpoint returned an unparseable response
Error message
Embedding endpoint returned an unparseable response (${safeUrl(url)}, batch ${batchIndex}) What it means
Inside the per-batch retry loop of httpEmbedBatch: the endpoint returned 2xx but the body could not be parsed as JSON. Raised as RetryableEmbeddingBodyError and retried with backoff under the process-global 'embeddings-http' breaker; this message surfaces (as HttpEmbeddingError) only after retries are exhausted. Genuine terminal network errors (abort-driven) are re-raised untouched so timeouts stay classified as timeouts.
Solutions
- curl the endpoint yourself: curl -sS $GITNEXUS_EMBEDDING_URL/embeddings -H "Authorization: Bearer $GITNEXUS_EMBEDDING_API_KEY" -d '{"model":"...","input":"x"}' — confirm JSON comes back.
- Fix the base URL (usually must include /v1; trailing slashes are trimmed automatically).
- Exclude the embeddings route from HTML-serving proxies/captive portals.
- If transient, retries already ran — check the endpoint's health and rerun the pipeline.
Example fix
# before export GITNEXUS_EMBEDDING_URL=http://localhost:11434 # UI host, not API # after export GITNEXUS_EMBEDDING_URL=http://localhost:11434/v1
Defensive patterns
Strategy: retry
Validate before calling
// smoke-test the endpoint before a pipeline run
const res = 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: 'ping' }),
});
const body = await res.text();
JSON.parse(body); // throws here first if the endpoint serves HTML Type guard
const isUnparseableBody = (e: unknown): boolean =>
e instanceof Error && e.message.includes('unparseable response'); Try / catch
try {
vectors = await httpEmbed(texts);
} catch (e) {
if (isUnparseableBody(e)) {
// endpoint contract broken: verify with curl, fix URL/proxy — internal retries already ran
} else throw e;
} Prevention
- Point GITNEXUS_EMBEDDING_URL at the API base (with /v1), never the web UI.
- Smoke-test /embeddings with curl after any endpoint or proxy change.
- Remember the client already retried with backoff before you see this — don't immediately re-run into the same broken endpoint.
When it happens
Trigger: An OpenAI-compatible /embeddings endpoint answering 200 with an HTML error page, empty body, BOM-prefixed JSON, or a proxy/CDN interstitial — happening on every attempt of a batch.
Common situations: Pointing GITNEXUS_EMBEDDING_URL at a web UI instead of the API base (missing /v1), an L7 proxy serving a captive portal, gzip/encoding issues, or a gateway returning HTML maintenance pages during deploys.
Related errors
- Embedding endpoint circuit open
- Embedding endpoint returned an unexpected response shape
- Embedding endpoint returned
- Embedding endpoint returned
- Embedding request cancelled
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/e08cb76fa8bcf040.
Report an issue: GitHub.
Appendix: 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 aac7515d2a)