Skip to content

fix: treat persistent 404 from external ingest search as empty result (fixes #330268) #330274

Description

@vs-code-engineering

Summary

Workspace chunk search's external ingest index throws an unhandled ExternalIngestRequestError: POST external-embeddings-code-search failed with status 404 when the remote embeddings index is not yet ready. The code already recognizes a 404 as the benign "index not ready yet" state and retries once after a 2s delay, but if the second attempt also returns 404 the error is re-thrown and surfaces as unhandled-error telemetry. Impact: noisy error telemetry (and a failed local-diff search leg) for a normal, transient service state during first index / large workspace.

Fixes #330268
Recommended reviewer: @mjbvz

Culprit Commit

Field Value
Commit 5e1ee93d
Author @mjbvz
PR #4493 (copilot-chat)
Message Add slight retry on first external ingest search failure
Why This commit introduced the single-retry workaround at externalIngestIndex.ts:485-489. It classifies a 404 as the expected "index not ready" state and retries once, but the retried call's own 404 is not caught, so a persistent not-ready state re-throws instead of degrading to an empty result.

Code Flow

sequenceDiagram
    participant Search as CodeSearchChunkSearch.searchLocalDiff
    participant Index as ExternalIngestIndex.search
    participant Client as ExternalIngestClient.searchFilesets
    participant API as githubApiFetcher

    Search->>Index: search(sizing, query)
    Index->>Client: searchFilesets() [attempt 1]
    Client->>API: POST /external/embeddings/code/search
    API-->>Client: 404 (index not ready)
    Note over Index: catch 404 -> wait 2s, retry once
    Index->>Client: searchFilesets() [attempt 2]
    Client->>API: POST /external/embeddings/code/search
    API-->>Client: 404 (still not ready)
    Note over Client: 💥 throw ExternalIngestRequestError:<br/>"POST external-embeddings-code-search failed with status 404"
    Client-->>Index: rethrow (not caught on retry)
    Note over Index: reported via externalIngestIndex.search.error -> unhandled telemetry
Loading

Affected Files

File Role Evidence
extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestClient.ts crash site L173: throw new ExternalIngestRequestError(...failed with status ${response.status})
extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestIndex.ts root cause L485-L490: if (err instanceof ExternalIngestRequestError && err.response.status === 404) { ... return await this._client.searchFilesets(...) } — retry re-throws on a persistent 404

Repro Steps

  1. Open a large workspace (or one being indexed for the first time) with Copilot workspace chunk search / external ingest enabled.
  2. Trigger a semantic workspace search (e.g. #codebase) while the remote embeddings index is still being built server-side.
  3. The first searchFilesets call returns 404 (index not ready); after a 2s delay a second call also returns 404 because indexing hasn't finished.
  4. The retried 404 re-throws and is recorded as an unhandled error (externalIngestIndex.search.error).

How the Fix Works

Chosen approachexternalIngestIndex.tsExternalIngestIndex.search(): wrap the single retry searchFilesets call in its own try/catch. When the retried call also fails with a 404, return undefined — the same value the method already produces for an empty/not-ready result, which the existing if (!searchResult || !searchResult.results) { return []; } guard at L505 turns into an empty result. Any non-404 error on the retry is still re-thrown unchanged, so genuine failures continue to surface via externalIngestIndex.search.error / the telemetry pipeline. This fixes the condition at its source: the code that already knows a 404 means "index not ready" now treats a persistent not-ready state as the benign empty result it is, rather than an error. No logService.error call is removed and no genuine error is swallowed — only the already-classified benign 404 is degraded to an empty result.

Alternatives considered: adding a broad try/catch at the outer searchLocalDiff/searchWorkspace call site — rejected because it would swallow unrelated errors and sits far from where 404 is already understood as benign, hiding real failures instead of handling the specific known-benign state at its origin.

Recommended Owner

@mjbvz — authored the culprit commit (5e1ee93d) and is the telemetry owner for externalIngestIndex.* events. Active in microsoft/vscode within the last 90 days.

Generated by errors-fix · opus48 · 415.2 AIC · ⌖ 11.3 AIC · ⊞ 18.6K ·


Note

This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch fix/external-ingest-persistent-404-8ef906b52b29e16a.

Original error: ERR_API: [2026-08-11T15:17:11.446Z] create pull request in microsoft/vscode failed (attempt 1)

Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.

To create the pull request manually:

gh pr create --title "fix: treat persistent 404 from external ingest search as empty result (fixes #330268)" --base main --head vscodebot-pr:fix/external-ingest-persistent-404-8ef906b52b29e16a --repo microsoft/vscode
Show patch (37 lines)
From b4b2f1c017bd0b836c8232c512f7d7e6f83ac69c Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Tue, 11 Aug 2026 15:10:20 +0000
Subject: [PATCH] fix: treat persistent 404 from external ingest search as
 empty result (fixes #330268)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../node/codeSearch/externalIngestIndex.ts            | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestIndex.ts b/extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestIndex.ts
index acc320a46f4..f74f8599eeb 100644
--- a/extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestIndex.ts
+++ b/extensions/copilot/src/platform/workspaceChunkSearch/node/codeSearch/externalIngestIndex.ts
@@ -486,7 +486,16 @@ export class ExternalIngestIndex extends Disposable {
 							// On the first index or a large workspace, there might be a slight delay on the service
 							// before the index is actually ready. Workaround by retrying just once after a short delay.
 							await raceCancellationError(timeout(2000), token);
-							return await this._client.searchFilesets(filesetName, resolvedQuery, sizing.maxResultCountHint, callTracker, token);
+							try {
+								return await this._client.searchFilesets(filesetName, resolvedQuery, sizing.maxResultCountHint, callTracker, token);
+							} catch (retryErr) {
+								if (retryErr instanceof ExternalIngestRequestError && retryErr.response.status === 404) {
+									// The index is still not ready. This is an expected, benign state (not an
+									// error condition), so treat it as an empty result rather than throwing.
+									return undefined;
+								}
+								throw retryErr;
+							}
 						}
 						throw err;
 					}
-- 
2.54.0

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions