{"record":{"id":"e08cb76fa8bcf040","repo":"abhigyanpatwari/GitNexus","slug":"embedding-endpoint-returned-an-unparseable-respons","errorCode":null,"errorMessage":"Embedding endpoint returned an unparseable response (${safeUrl(url)}, batch ${batchIndex})","messagePattern":"Embedding endpoint returned an unparseable response \\((.+?), batch (.+?)\\)","errorType":"exception","errorClass":"RetryableEmbeddingBodyError","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/embeddings/http-client.ts","lineNumber":460,"sourceCode":"          // and the same breaker accounting as any other endpoint fault (#2790).\n          let payload: { data: EmbeddingItem[] };\n          try {\n            payload = (await attemptResp.json()) as { data: EmbeddingItem[] };\n          } catch (err) {\n            // Not every `.json()` rejection is a parse error: the per-attempt\n            // signal (`AbortSignal.any([caller, AbortSignal.timeout(...)])`) is\n            // wired to the body stream, so a stalled body rejects with the abort\n            // reason. Re-raise those untouched — `isTerminalNetworkError` is\n            // `resilientFetch`'s own predicate, so this test agrees with\n            // `classifyOutcome` by construction. Wrapping one would flip its\n            // verdict from `terminal-network` (returned without retry AND\n            // without touching the breaker, via `recordNeutral()`) to\n            // `retryable-network` (retried, then `breaker.recordFailure()`): the\n            // same timeout would take 3 attempts instead of 1, count toward the\n            // process-global `embeddings-http` breaker, and reach the operator as\n            // \"unparseable response\" so they never reach for the timeout knob.\n            if (isTerminalNetworkError(err)) throw err;\n            throw new RetryableEmbeddingBodyError(unparseableMessage(), { cause: err });\n          }\n          if (!Array.isArray(payload?.data) || !payload.data.every(isEmbeddingItem)) {\n            throw new RetryableEmbeddingBodyError(unexpectedShapeMessage());\n          }\n          // Cardinality belongs *inside* the retry loop. `every(isEmbeddingItem)`\n          // is vacuously true for `[]` and true for any array shorter than the\n          // request, so a 200 carrying `{\"data\": []}` — or half the vectors —\n          // used to be classified `success`, call `breaker.recordSuccess()`\n          // (erasing the outage signal), and only then fail terminally after a\n          // single attempt. A short body is a truncated body: same backoff, same\n          // breaker accounting as any other endpoint fault (#2790).\n          if (payload.data.length !== batch.length) {\n            throw new RetryableEmbeddingBodyError(\n              countMismatchMessage(payload.data.length, batch.length, safeUrl(url), batchIndex),\n            );\n          }\n          parsed = payload.data;\n          return attemptResp;","sourceCodeStart":442,"sourceCodeEnd":478,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus/src/core/embeddings/http-client.ts#L442-L478","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["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.","Correct GITNEXUS_EMBEDDING_URL to the OpenAI-compatible base (no trailing /embeddings; the client appends it), e.g. https://api.openai.com/v1.","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.","Check ingress/proxy buffering and gzip settings on the embedding service."],"exampleFix":"// before: URL points at the marketing site\nexport GITNEXUS_EMBEDDING_URL=https://provider.example.com\n\n// after: URL points at the OpenAI-compatible API base\nexport GITNEXUS_EMBEDDING_URL=https://provider.example.com/v1","handlingStrategy":"retry","validationCode":"// Probe the endpoint once before the indexing run:\nconst probe = await fetch(`${process.env.GITNEXUS_EMBEDDING_URL}/embeddings`, {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    Authorization: `Bearer ${process.env.GITNEXUS_EMBEDDING_API_KEY}`,\n  },\n  body: JSON.stringify({ model: process.env.GITNEXUS_EMBEDDING_MODEL, input: 'probe' }),\n});\nconst ct = probe.headers.get('content-type') ?? '';\nif (!ct.includes('application/json')) {\n  throw new Error(`Endpoint is not JSON (content-type: ${ct}); check GITNEXUS_EMBEDDING_URL`);\n}","typeGuard":"// RetryableEmbeddingBodyError is module-private; catch its terminal form instead.\nimport { isHttpEmbeddingError } from 'gitnexus';\nconst isUnparseableTerminal = (e: unknown): boolean =>\n  isHttpEmbeddingError(e) &&\n  e instanceof Error &&\n  e.message.includes('returned an unparseable response');","tryCatchPattern":"// The library already retries inside resilientFetch; at the outer boundary,\n// treat a persistent unparseable body as a configuration fault.\ntry {\n  await httpEmbed(texts);\n} catch (e) {\n  if (isHttpEmbeddingError(e) && e.message.includes('unparseable response')) {\n    // surface to operator: endpoint is returning a non-JSON body\n  }\n  throw e;\n}","preventionTips":["curl the endpoint once to confirm it returns application/json before launching a long indexing run.","Point GITNEXUS_EMBEDDING_URL at the API base (e.g. .../v1), not a homepage or console.","If you front the endpoint with a proxy, disable HTML error pages for the embeddings route."],"tags":["network","retry","endpoint","response-body","embeddings"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}