feat(copilot): opt-in HTTP cache for the Node fetch fetcher#317721
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds opt-in HTTP caching support to Copilot’s Node fetch path (undici-based) while ensuring cached requests still route through VS Code’s proxy/CA-injection patching, and surfaces cache outcomes via Response.cacheStatus plus fetch telemetry.
Changes:
- Exposes a host-provided
__vscodeCreateFetchPatch({ interceptors })factory to compose additional undici interceptors on top of the existing proxy-aware fetch patch. - Adds an opt-in cache hint (
FetchOptions.cache?: boolean) with cache status propagation (Response.cacheStatus, telemetry) and a debug setting to choose cache mode (off/memory/persistent). - Introduces a
taggedCacheInterceptorwrapper that stamps cache outcome onto responses for downstream attribution.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/api/node/proxyResolver.ts | Exposes a fetch-patch factory on globalThis to allow composing additional undici interceptors while preserving proxy/CA patching. |
| extensions/copilot/src/platform/telemetry/vscode-node/telemetryServiceImpl.ts | Adds cacheStatus to fetch telemetry payload/classification and emits it as metadata. |
| extensions/copilot/src/platform/networking/vscode-node/test/fetcherServiceCrash.spec.ts | Updates tests for the new FetcherService constructor dependency. |
| extensions/copilot/src/platform/networking/vscode-node/fetcherServiceImpl.ts | Wires cache mode setting and persistent store location into NodeFetchFetcher, and forwards cacheStatus into fetch telemetry events. |
| extensions/copilot/src/platform/networking/node/test/taggedCacheInterceptor.spec.ts | Adds unit tests for cache-status tagging and classification behavior. |
| extensions/copilot/src/platform/networking/node/taggedCacheInterceptor.ts | Implements the cache interceptor wrapper that stamps cache outcome onto response headers. |
| extensions/copilot/src/platform/networking/node/nodeFetchFetcher.ts | Adds opt-in cache routing via a request marker and composes the cache interceptor using the host fetch-patch factory. |
| extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts | Adds a request-init hook and reads stamped cache status to populate Response.cacheStatus. |
| extensions/copilot/src/platform/networking/common/fetcherService.ts | Extends fetch options and response/telemetry types with cache-related fields. |
| extensions/copilot/src/platform/configuration/common/configurationService.ts | Adds the advanced.debug.nodeFetchCache setting (off/memory/persistent). |
Comments suppressed due to low confidence (2)
extensions/copilot/src/platform/networking/node/taggedCacheInterceptor.ts:86
readHeaderreturns only the first element when the header value is astring[]. For headers likewarningthis can drop additional warnings and lead to incorrectstale-hitclassification. Consider checking all values (and/or joining) when the value is an array.
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value) && typeof value[0] === 'string') {
return value[0];
extensions/copilot/src/platform/networking/node/taggedCacheInterceptor.ts:94
stampStatusskips stamping when undici supplies headers in raw wire format (string[]). In that caseBaseFetchFetcherwill always reportcacheStatus: 'bypass'even if the cache served the response. Consider supporting the array form by appending the header key/value pair (and/or normalizing to an object) before forwarding to the downstream handler.
function stampStatus(headers: unknown, status: CacheStatus): void {
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
(headers as Record<string, string>)[VSCODE_CACHE_STATUS_HEADER] = status;
}
c097dad to
85c8868
Compare
Adds an opportunistic cache support to the Copilot Node fetch path. The cache
is strictly opt-in per request and composes with the existing VSCode proxy
and CA-injection patch.
- `__vscodeCreateFetchPatch({ interceptors })` lets the extension host
build a second proxy-aware `fetch` with extra undici interceptors. The
default `__vscodePatchedFetch` is unchanged.
- `NodeFetchFetcher` builds an undici cache interceptor once at
construction time and uses the factory to produce a `cachedFetch` that
routes through both the proxy patch and the cache. Requests are tagged
with an internal `__copilotCachePatch` marker (stripped before fetch);
unmarked requests keep going through the regular patched fetch. When
the host lacks the factory, caching is silently disabled so requests
never bypass the proxy patch.
- `FetchOptions.cache?: boolean` — opportunistic hint. Fetchers without
cache support ignore it; fallback to other fetchers is unaffected.
- `Response.cacheStatus` and `FetchTelemetryEvent.cacheStatus`:
`'hit' | 'stale-hit' | 'revalidated' | 'miss' | 'bypass'`.
- New setting `github.copilot.advanced.debug.nodeFetchCache`:
`'off' | 'memory' | 'persistent'` (default `'memory'`). `'persistent'`
uses undici's SQLite store under the extension's global storage
(`undici-cache.v1.sqlite`) when available, otherwise falls back to
memory.
- New `taggedCacheInterceptor` wraps `undici.interceptors.cache` and
stamps a private `VSCODE_CACHE_STATUS_HEADER` on the response so the
base fetcher can read the outcome without parsing undici internals.
- `BaseFetchFetcher` exposes an overridable `_buildRequestInit` hook and
reports `cacheStatus` on `Response` and `fetchTelemetry`.
Notes
- No behavior change for callers that don't set `cache: true`.
- The cache interceptor is constructed once per fetcher instance; the
composed dispatcher chain is reused so connection pooling is preserved.
- Depends on microsoft/vscode-proxy-agent#100
For #308310
- drop the `age` header gate from classify(): undici's cache interceptor only adds if-modified-since / if-none-match when revalidating a stored entry, so `state.conditional` alone is a sufficient signal. The age header is not guaranteed on a revalidated 200, which caused 'revalidated' to be misreported as 'miss'. - the etag integration test used `cache-control: max-age=0, must-revalidate`, which undici treats as already-stale on arrival and refuses to store (cache-handler.js bails when `now >= absoluteStaleAt`), so there was nothing to revalidate on the second call. Switch the origin to `public, max-age=60` and pass `cache-control: no-cache` on the second request to drive undici's needsRevalidation() path, which dispatches with if-none-match and serves the cached body on 304.
85c8868 to
cd23554
Compare
roblourens
approved these changes
May 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an opportunistic cache support to the Copilot Node fetch path. The cache is strictly opt-in per request and composes with the existing VSCode proxy and CA-injection patch.
__vscodeCreateFetchPatch({ interceptors })lets the extension host build a second proxy-awarefetchwith extra undici interceptors. The default__vscodePatchedFetchis unchanged.NodeFetchFetcherbuilds an undici cache interceptor once at construction time and uses the factory to produce acachedFetchthat routes through both the proxy patch and the cache. Requests are tagged with an internal__copilotCachePatchmarker (stripped before fetch); unmarked requests keep going through the regular patched fetch. When the host lacks the factory, caching is silently disabled so requests never bypass the proxy patch.FetchOptions.cache?: boolean— opportunistic hint. Fetchers without cache support ignore it; fallback to other fetchers is unaffected.Response.cacheStatusandFetchTelemetryEvent.cacheStatus:'hit' | 'stale-hit' | 'revalidated' | 'miss' | 'bypass'.github.copilot.advanced.debug.nodeFetchCache:'off' | 'memory' | 'persistent'(default'memory').'persistent'uses undici's SQLite store under the extension's global storage (undici-cache.v1.sqlite) when available, otherwise falls back to memory.taggedCacheInterceptorwrapsundici.interceptors.cacheand stamps a privateVSCODE_CACHE_STATUS_HEADERon the response so the base fetcher can read the outcome without parsing undici internals.BaseFetchFetcherexposes an overridable_buildRequestInithook and reportscacheStatusonResponseandfetchTelemetry.Notes
cache: true.For #308310