{"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/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/core/embeddings/http-client.ts#L442-L478","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nexport GITNEXUS_EMBEDDING_URL=http://localhost:11434   # UI host, not API\n# after\nexport GITNEXUS_EMBEDDING_URL=http://localhost:11434/v1","handlingStrategy":"retry","validationCode":"// smoke-test the endpoint before a pipeline run\nconst res = await fetch(`${process.env.GITNEXUS_EMBEDDING_URL}/embeddings`, {\n  method: 'POST',\n  headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.GITNEXUS_EMBEDDING_API_KEY}` },\n  body: JSON.stringify({ model: process.env.GITNEXUS_EMBEDDING_MODEL, input: 'ping' }),\n});\nconst body = await res.text();\nJSON.parse(body); // throws here first if the endpoint serves HTML","typeGuard":"const isUnparseableBody = (e: unknown): boolean =>\n  e instanceof Error && e.message.includes('unparseable response');","tryCatchPattern":"try {\n  vectors = await httpEmbed(texts);\n} catch (e) {\n  if (isUnparseableBody(e)) {\n    // endpoint contract broken: verify with curl, fix URL/proxy — internal retries already ran\n  } else throw e;\n}","preventionTips":["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."],"tags":["embeddings","http","json","response-parsing","retry"],"backgroundTag":"malformed-api-response","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}