mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
feat(copilot): opt-in HTTP cache for the Node fetch fetcher (#317721)
* feat(copilot): opt-in HTTP cache for the Node fetch fetcher
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 https://github.com/microsoft/vscode-proxy-agent/pull/100
For https://github.com/microsoft/vscode/issues/308310
* fix: undici integration tests
- 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.
* chore: fix lint
* chore: cleanup cache marker in favor of function arg
This commit is contained in:
@@ -585,6 +585,7 @@ export namespace ConfigKey {
|
||||
export const DebugUseNodeFetchFetcher = defineSetting('advanced.debug.useNodeFetchFetcher', ConfigType.Simple, true);
|
||||
export const DebugUseNodeFetcher = defineSetting('advanced.debug.useNodeFetcher', ConfigType.Simple, false);
|
||||
export const DebugUseElectronFetcher = defineSetting('advanced.debug.useElectronFetcher', ConfigType.Simple, true);
|
||||
export const DebugNodeFetchCache = defineSetting<'off' | 'memory' | 'persistent'>('advanced.debug.nodeFetchCache', ConfigType.Simple, 'memory');
|
||||
export const AuthProvider = defineSetting<AuthProviderId>('advanced.authProvider', ConfigType.Simple, AuthProviderId.GitHub);
|
||||
export const AuthPermissions = defineSetting<AuthPermissionMode>('advanced.authPermissions', ConfigType.Simple, AuthPermissionMode.Default);
|
||||
}
|
||||
|
||||
@@ -71,8 +71,16 @@ export interface FetchTelemetryEvent {
|
||||
latencyMs: number;
|
||||
statusCode: number | undefined;
|
||||
success: boolean;
|
||||
/**
|
||||
* Cache outcome when the caller opted in via {@link FetchOptions.cache}.
|
||||
* `undefined` when the caller did not opt in or the selected fetcher does
|
||||
* not implement caching.
|
||||
*/
|
||||
cacheStatus?: CacheStatus;
|
||||
}
|
||||
|
||||
export type CacheStatus = 'hit' | 'stale-hit' | 'revalidated' | 'miss' | 'bypass';
|
||||
|
||||
/** A basic version of http://developer.mozilla.org/en-US/docs/Web/API/Response */
|
||||
export class Response {
|
||||
ok = this.status >= 200 && this.status < 300;
|
||||
@@ -92,6 +100,7 @@ export class Response {
|
||||
private readonly _reportEvent: ReportFetchEvent,
|
||||
private readonly _internalId: string,
|
||||
private readonly _hostname: string,
|
||||
readonly cacheStatus?: CacheStatus,
|
||||
) {
|
||||
const transformer = {
|
||||
transform: (chunk: Uint8Array, controller: TransformStreamDefaultController<Uint8Array>) => {
|
||||
@@ -169,6 +178,13 @@ export interface FetchOptions {
|
||||
expectJSON?: boolean;
|
||||
useFetcher?: FetcherId;
|
||||
suppressIntegrationId?: boolean;
|
||||
/**
|
||||
* Opportunistic cache hint. When set to `true`, fetchers that implement
|
||||
* caching (today only the Node fetch fetcher) will route the request
|
||||
* through an RFC 9111 cache. Fetchers without cache support ignore this
|
||||
* flag, and fallback to other fetchers continues to work normally.
|
||||
*/
|
||||
cache?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginationOptions<T> extends FetchOptions {
|
||||
|
||||
@@ -7,13 +7,20 @@
|
||||
import { generateUuid } from '../../../util/vs/base/common/uuid';
|
||||
import { IEnvService } from '../../env/common/envService';
|
||||
import { collectSingleLineErrorMessage } from '../../log/common/logService';
|
||||
import { FetcherId, FetchOptions, IAbortController, isAbortError, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
|
||||
import { CacheStatus, FetcherId, FetchOptions, IAbortController, isAbortError, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
|
||||
import { IFetcher, userAgentLibraryHeader } from '../common/networking';
|
||||
import { VSCODE_CACHE_STATUS_HEADER } from './taggedCacheInterceptor';
|
||||
|
||||
export type FetchImpl = (
|
||||
input: string | globalThis.Request,
|
||||
init?: RequestInit,
|
||||
useCache?: boolean,
|
||||
) => Promise<globalThis.Response>;
|
||||
|
||||
export abstract class BaseFetchFetcher implements IFetcher {
|
||||
|
||||
constructor(
|
||||
private readonly _fetchImpl: typeof fetch | typeof import('electron').net.fetch,
|
||||
private readonly _fetchImpl: FetchImpl,
|
||||
private readonly _envService: IEnvService,
|
||||
private readonly _fetcherId: FetcherId,
|
||||
private readonly _reportEvent: ReportFetchEvent,
|
||||
@@ -52,7 +59,7 @@ export abstract class BaseFetchFetcher implements IFetcher {
|
||||
const internalId = generateUuid();
|
||||
const hostname = safeGetHostname(url);
|
||||
try {
|
||||
const response = await this._fetch(url, method, headers, body, signal, internalId, hostname);
|
||||
const response = await this._fetch(url, method, headers, body, signal, internalId, hostname, options);
|
||||
this._reportEvent({ internalId, timestamp: Date.now(), outcome: 'success', phase: 'requestResponse', fetcher: this._fetcherId, hostname, statusCode: response.status });
|
||||
return response;
|
||||
} catch (e) {
|
||||
@@ -89,8 +96,8 @@ export abstract class BaseFetchFetcher implements IFetcher {
|
||||
return items;
|
||||
}
|
||||
|
||||
private async _fetch(url: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', headers: { [name: string]: string }, body: string | undefined, signal: AbortSignal, internalId: string, hostname: string): Promise<Response> {
|
||||
const resp = await this._fetchImpl(url, { method, headers, body, signal });
|
||||
private async _fetch(url: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', headers: { [name: string]: string }, body: string | undefined, signal: AbortSignal, internalId: string, hostname: string, options: FetchOptions): Promise<Response> {
|
||||
const resp = await this._fetchImpl(url, { method, headers, body, signal }, !!options.cache);
|
||||
return new Response(
|
||||
resp.status,
|
||||
resp.statusText,
|
||||
@@ -100,9 +107,24 @@ export abstract class BaseFetchFetcher implements IFetcher {
|
||||
this._reportEvent,
|
||||
internalId,
|
||||
hostname,
|
||||
this._readCacheStatus(resp.headers, options),
|
||||
);
|
||||
}
|
||||
|
||||
private _readCacheStatus(headers: { get(name: string): string | null }, options: FetchOptions): CacheStatus | undefined {
|
||||
if (!options.cache) {
|
||||
return undefined;
|
||||
}
|
||||
const stamped = headers.get(VSCODE_CACHE_STATUS_HEADER);
|
||||
if (stamped === 'hit' || stamped === 'stale-hit' || stamped === 'revalidated' || stamped === 'miss') {
|
||||
return stamped;
|
||||
}
|
||||
// Caller opted in but the response carried no marker — either the
|
||||
// fetcher does not implement caching or the cache interceptor skipped
|
||||
// this request.
|
||||
return 'bypass';
|
||||
}
|
||||
|
||||
async disconnectAll(): Promise<void> {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@@ -8,7 +8,22 @@ import * as undici from 'undici';
|
||||
import { Lazy } from '../../../util/vs/base/common/lazy';
|
||||
import { IEnvService } from '../../env/common/envService';
|
||||
import { HeadersImpl, IHeaders, ReportFetchEvent, WebSocketConnection, WebSocketConnectOptions } from '../common/fetcherService';
|
||||
import { BaseFetchFetcher } from './baseFetchFetcher';
|
||||
import { BaseFetchFetcher, FetchImpl } from './baseFetchFetcher';
|
||||
import { taggedCacheInterceptor } from './taggedCacheInterceptor';
|
||||
|
||||
type CacheInterceptorOptions = NonNullable<Parameters<typeof undici.interceptors.cache>[0]>;
|
||||
type CacheStore = NonNullable<CacheInterceptorOptions['store']>;
|
||||
|
||||
type FetchPatchFactory = (options?: {
|
||||
interceptors?: readonly undici.Dispatcher.DispatcherComposeInterceptor[];
|
||||
}) => typeof globalThis.fetch;
|
||||
|
||||
export type NodeFetchCacheMode = 'off' | 'memory' | 'persistent';
|
||||
|
||||
export interface NodeFetchCacheOptions {
|
||||
readonly mode: NodeFetchCacheMode;
|
||||
readonly storeLocation?: string;
|
||||
}
|
||||
|
||||
export class NodeFetchFetcher extends BaseFetchFetcher {
|
||||
|
||||
@@ -18,8 +33,14 @@ export class NodeFetchFetcher extends BaseFetchFetcher {
|
||||
envService: IEnvService,
|
||||
reportEvent: ReportFetchEvent = () => { },
|
||||
userAgentLibraryUpdate?: (original: string) => string,
|
||||
cacheOptions: NodeFetchCacheOptions = { mode: 'memory' },
|
||||
) {
|
||||
super(getFetch(), envService, NodeFetchFetcher.ID, reportEvent, userAgentLibraryUpdate);
|
||||
// Caching requires the host-provided fetch-patch factory so cached requests
|
||||
// still go through the proxy/CA-injection patch. On older hosts that lack
|
||||
// the factory, caching is silently disabled.
|
||||
const factory = (globalThis as any).__vscodeCreateFetchPatch as FetchPatchFactory | undefined;
|
||||
const interceptor = cacheOptions.mode !== 'off' && factory ? createCacheInterceptor(cacheOptions) : undefined;
|
||||
super(getFetch(interceptor, factory), envService, NodeFetchFetcher.ID, reportEvent, userAgentLibraryUpdate);
|
||||
}
|
||||
|
||||
getUserAgentLibrary(): string {
|
||||
@@ -35,10 +56,44 @@ export class NodeFetchFetcher extends BaseFetchFetcher {
|
||||
}
|
||||
}
|
||||
|
||||
function getFetch(): typeof globalThis.fetch {
|
||||
const fetch = (globalThis as any).__vscodePatchedFetch || globalThis.fetch;
|
||||
return function (input: string | URL | globalThis.Request, init?: RequestInit) {
|
||||
return fetch(input, { dispatcher: agent.value, ...init });
|
||||
function createCacheInterceptor(options: NodeFetchCacheOptions): undici.Dispatcher.DispatcherComposeInterceptor | undefined {
|
||||
const store = createCacheStore(options);
|
||||
if (!store) {
|
||||
return undefined;
|
||||
}
|
||||
return taggedCacheInterceptor({ store, type: 'private' });
|
||||
}
|
||||
|
||||
function createCacheStore(options: NodeFetchCacheOptions): CacheStore | undefined {
|
||||
if (options.mode === 'persistent') {
|
||||
const SqliteCacheStore = (undici as unknown as { cacheStores?: { SqliteCacheStore?: new (init?: object) => CacheStore } }).cacheStores?.SqliteCacheStore;
|
||||
if (SqliteCacheStore && options.storeLocation) {
|
||||
try {
|
||||
return new SqliteCacheStore({
|
||||
location: options.storeLocation,
|
||||
maxCount: 5000,
|
||||
maxEntrySize: 5 * 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
const MemoryCacheStore = (undici as unknown as { cacheStores?: { MemoryCacheStore?: new (init?: object) => CacheStore } }).cacheStores?.MemoryCacheStore;
|
||||
if (!MemoryCacheStore) {
|
||||
return undefined;
|
||||
}
|
||||
return new MemoryCacheStore({ maxCount: 1000, maxEntrySize: 5 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
function getFetch(cacheInterceptor: undici.Dispatcher.DispatcherComposeInterceptor | undefined, createFetchPatch: FetchPatchFactory | undefined): FetchImpl {
|
||||
const defaultFetch = (globalThis as any).__vscodePatchedFetch || globalThis.fetch;
|
||||
const cachedFetch = cacheInterceptor && createFetchPatch ? createFetchPatch({ interceptors: [cacheInterceptor] }) : undefined;
|
||||
return function (input, init, useCache) {
|
||||
if (useCache && cachedFetch) {
|
||||
return cachedFetch(input, init);
|
||||
}
|
||||
const dispatcher = (init as { dispatcher?: undici.Dispatcher } | undefined)?.dispatcher ?? agent.value;
|
||||
return defaultFetch(input, { ...init, dispatcher });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as undici from 'undici';
|
||||
|
||||
/**
|
||||
* Internal response header stamped by {@link taggedCacheInterceptor} so the
|
||||
* fetcher can attribute a response to the cache for telemetry purposes.
|
||||
*/
|
||||
export const VSCODE_CACHE_STATUS_HEADER = 'x-vscode-cache-status';
|
||||
|
||||
export type CacheStatus = 'hit' | 'stale-hit' | 'revalidated' | 'miss';
|
||||
|
||||
/**
|
||||
* Wraps undici's built-in `interceptors.cache(...)` and stamps the served
|
||||
* response with {@link VSCODE_CACHE_STATUS_HEADER} so downstream code can
|
||||
* tell whether the response came from the cache without relying on header
|
||||
* heuristics.
|
||||
*
|
||||
* Detection is based on observing whether the cache interceptor invoked the
|
||||
* downstream dispatcher for a given request and whether it added conditional
|
||||
* revalidation headers — both of which are deterministic for a given code
|
||||
* path in `undici/lib/interceptor/cache.js`.
|
||||
*/
|
||||
export function taggedCacheInterceptor(
|
||||
cacheOpts: Parameters<typeof undici.interceptors.cache>[0],
|
||||
): undici.Dispatcher.DispatcherComposeInterceptor {
|
||||
const cacheInterceptor = undici.interceptors.cache(cacheOpts);
|
||||
|
||||
return (dispatch) => (opts, handler) => {
|
||||
const state = { networkCalled: false, conditional: false };
|
||||
|
||||
const countingDispatch: typeof dispatch = (dOpts, dHandler) => {
|
||||
state.networkCalled = true;
|
||||
const h = dOpts.headers as Record<string, string> | undefined;
|
||||
state.conditional = !!(h && (h['if-modified-since'] || h['if-none-match']));
|
||||
return dispatch(dOpts, dHandler);
|
||||
};
|
||||
|
||||
const taggingHandler = new Proxy(handler, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'onResponseStart') {
|
||||
return (
|
||||
controller: Parameters<NonNullable<undici.Dispatcher.DispatchHandler['onResponseStart']>>[0],
|
||||
statusCode: number,
|
||||
headers: unknown,
|
||||
statusMessage?: string,
|
||||
) => {
|
||||
const status = classify(state, headers);
|
||||
stampStatus(headers, status);
|
||||
const orig = Reflect.get(target, prop, receiver) as undici.Dispatcher.DispatchHandler['onResponseStart'];
|
||||
return orig?.call(target, controller, statusCode, headers as Parameters<NonNullable<undici.Dispatcher.DispatchHandler['onResponseStart']>>[2], statusMessage);
|
||||
};
|
||||
}
|
||||
const value = Reflect.get(target, prop, receiver);
|
||||
return typeof value === 'function' ? value.bind(target) : value;
|
||||
},
|
||||
}) as undici.Dispatcher.DispatchHandler;
|
||||
|
||||
return cacheInterceptor(countingDispatch)(opts, taggingHandler);
|
||||
};
|
||||
}
|
||||
|
||||
function classify(
|
||||
state: { networkCalled: boolean; conditional: boolean },
|
||||
headers: unknown,
|
||||
): CacheStatus {
|
||||
if (!state.networkCalled) {
|
||||
return isStaleWarning(headers) ? 'stale-hit' : 'hit';
|
||||
}
|
||||
if (state.conditional) {
|
||||
return 'revalidated';
|
||||
}
|
||||
return 'miss';
|
||||
}
|
||||
|
||||
function isStaleWarning(headers: unknown): boolean {
|
||||
const value = readHeader(headers, 'warning');
|
||||
return typeof value === 'string' && value.startsWith('110');
|
||||
}
|
||||
|
||||
function readHeader(headers: unknown, name: string): string | undefined {
|
||||
if (!headers || typeof headers !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const value = (headers as Record<string, unknown>)[name];
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value) && typeof value[0] === 'string') {
|
||||
return value[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type * as undici from 'undici';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('undici', () => {
|
||||
const stub = {
|
||||
interceptors: { cache: vi.fn() },
|
||||
};
|
||||
return { ...stub, default: stub };
|
||||
});
|
||||
|
||||
type FakeDispatch = (opts: { headers?: Record<string, string> }, handler: undici.Dispatcher.DispatchHandler) => boolean;
|
||||
type CacheMiddleware = (dispatch: FakeDispatch) => FakeDispatch;
|
||||
|
||||
async function importTagger() {
|
||||
vi.resetModules();
|
||||
const { taggedCacheInterceptor, VSCODE_CACHE_STATUS_HEADER } = await import('../taggedCacheInterceptor');
|
||||
const undiciMock = (await import('undici')) as unknown as { interceptors: { cache: ReturnType<typeof vi.fn> } };
|
||||
return { taggedCacheInterceptor, VSCODE_CACHE_STATUS_HEADER, undiciMock };
|
||||
}
|
||||
|
||||
function makeController(): undici.Dispatcher.DispatchController {
|
||||
return {} as undici.Dispatcher.DispatchController;
|
||||
}
|
||||
|
||||
async function runTagger(middleware: CacheMiddleware): Promise<{ stamped: string | undefined; downstream: ReturnType<typeof vi.fn> }> {
|
||||
const { taggedCacheInterceptor, VSCODE_CACHE_STATUS_HEADER, undiciMock } = await importTagger();
|
||||
undiciMock.interceptors.cache.mockReturnValue(middleware);
|
||||
|
||||
const downstream = vi.fn(((_opts: { headers?: Record<string, string> }, _handler: undici.Dispatcher.DispatchHandler) => true) as FakeDispatch);
|
||||
const tagger = taggedCacheInterceptor({} as Parameters<typeof undici.interceptors.cache>[0]);
|
||||
const intercepted = tagger(downstream as unknown as undici.Dispatcher['dispatch']);
|
||||
|
||||
let stamped: string | undefined;
|
||||
intercepted({ headers: {} } as undici.Dispatcher.DispatchOptions, {
|
||||
onResponseStart: (_c, _s, headers) => {
|
||||
stamped = (headers as Record<string, string>)[VSCODE_CACHE_STATUS_HEADER];
|
||||
},
|
||||
});
|
||||
return { stamped, downstream };
|
||||
}
|
||||
|
||||
describe('taggedCacheInterceptor', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('classifies a cache hit when the downstream dispatch is never invoked', async () => {
|
||||
const { stamped, downstream } = await runTagger(() => (_opts, handler) => {
|
||||
handler.onResponseStart?.(makeController(), 200, { age: '60' }, 'OK');
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(stamped).toBe('hit');
|
||||
expect(downstream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('classifies a stale hit when the served response carries a 110 warning', async () => {
|
||||
const { stamped } = await runTagger(() => (_opts, handler) => {
|
||||
handler.onResponseStart?.(makeController(), 200, { age: '120', warning: '110 - "response is stale"' }, 'OK');
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(stamped).toBe('stale-hit');
|
||||
});
|
||||
|
||||
it('classifies a revalidation when conditional headers reach the origin', async () => {
|
||||
const { stamped, downstream } = await runTagger((dispatch) => (_opts, handler) => {
|
||||
dispatch({ headers: { 'if-none-match': '"abc"' } }, {} as undici.Dispatcher.DispatchHandler);
|
||||
handler.onResponseStart?.(makeController(), 200, {}, 'OK');
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(stamped).toBe('revalidated');
|
||||
expect(downstream).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('classifies a miss when the cache passes the request through unchanged', async () => {
|
||||
const { stamped, downstream } = await runTagger((dispatch) => (opts, handler) => {
|
||||
dispatch(opts, {} as undici.Dispatcher.DispatchHandler);
|
||||
handler.onResponseStart?.(makeController(), 200, {}, 'OK');
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(stamped).toBe('miss');
|
||||
expect(downstream).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not throw when response headers are an array (raw wire format)', async () => {
|
||||
const { taggedCacheInterceptor, undiciMock } = await importTagger();
|
||||
const middleware: CacheMiddleware = () => (_opts, handler) => {
|
||||
handler.onResponseStart?.(makeController(), 200, ['age', '60'] as unknown as Record<string, string>, 'OK');
|
||||
return true;
|
||||
};
|
||||
undiciMock.interceptors.cache.mockReturnValue(middleware);
|
||||
|
||||
const tagger = taggedCacheInterceptor({} as Parameters<typeof undici.interceptors.cache>[0]);
|
||||
const intercepted = tagger((() => true) as unknown as undici.Dispatcher['dispatch']);
|
||||
expect(() => intercepted({ headers: {} } as undici.Dispatcher.DispatchOptions, { onResponseStart: () => { } })).not.toThrow();
|
||||
});
|
||||
|
||||
it('forwards prototype-defined handler methods through the tagging proxy', async () => {
|
||||
const { taggedCacheInterceptor, undiciMock } = await importTagger();
|
||||
const middleware: CacheMiddleware = () => (_opts, handler) => {
|
||||
handler.onResponseStart?.(makeController(), 200, {}, 'OK');
|
||||
handler.onResponseData?.(makeController(), Buffer.from('chunk'));
|
||||
handler.onResponseEnd?.(makeController(), {});
|
||||
return true;
|
||||
};
|
||||
undiciMock.interceptors.cache.mockReturnValue(middleware);
|
||||
|
||||
const calls: string[] = [];
|
||||
class ProtoHandler {
|
||||
onResponseStart() { calls.push('start'); }
|
||||
onResponseData() { calls.push('data'); }
|
||||
onResponseEnd() { calls.push('end'); }
|
||||
}
|
||||
|
||||
const tagger = taggedCacheInterceptor({} as Parameters<typeof undici.interceptors.cache>[0]);
|
||||
const intercepted = tagger((() => true) as unknown as undici.Dispatcher['dispatch']);
|
||||
intercepted({ headers: {} } as undici.Dispatcher.DispatchOptions, new ProtoHandler() as unknown as undici.Dispatcher.DispatchHandler);
|
||||
|
||||
expect(calls).toEqual(['start', 'data', 'end']);
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,13 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { Emitter } from '../../../util/vs/base/common/event';
|
||||
import { Disposable } from '../../../util/vs/base/common/lifecycle';
|
||||
import { Config, ConfigKey, ExperimentBasedConfig, ExperimentBasedConfigType, IConfigurationService } from '../../configuration/common/configurationService';
|
||||
import { INTEGRATION_ID } from '../../endpoint/common/licenseAgreement';
|
||||
import { IEnvService } from '../../env/common/envService';
|
||||
import { IVSCodeExtensionContext } from '../../extContext/common/extensionContext';
|
||||
import { ILogService } from '../../log/common/logService';
|
||||
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
|
||||
import { ITelemetryService } from '../../telemetry/common/telemetry';
|
||||
@@ -15,7 +17,7 @@ import { FetchEvent, FetchOptions, FetchTelemetryEvent, IAbortController, IFetch
|
||||
import { IFetcher } from '../common/networking';
|
||||
import { fetchWithFallbacks } from '../node/fetcherFallback';
|
||||
import { NodeFetcher } from '../node/nodeFetcher';
|
||||
import { createWebSocket, NodeFetchFetcher } from '../node/nodeFetchFetcher';
|
||||
import { createWebSocket, NodeFetchCacheMode, NodeFetchCacheOptions, NodeFetchFetcher } from '../node/nodeFetchFetcher';
|
||||
import { ElectronFetcher } from './electronFetcher';
|
||||
|
||||
export class FetcherService extends Disposable implements IFetcherService {
|
||||
@@ -35,6 +37,7 @@ export class FetcherService extends Disposable implements IFetcherService {
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IEnvService private readonly _envService: IEnvService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext,
|
||||
) {
|
||||
super();
|
||||
this._availableFetchers = fetcher ? [fetcher] : undefined;
|
||||
@@ -106,7 +109,7 @@ export class FetcherService extends Disposable implements IFetcherService {
|
||||
}
|
||||
|
||||
// Node fetch preferred over Node https in fallbacks. (HTTP2 support)
|
||||
const nodeFetchFetcher = new NodeFetchFetcher(envService, reportEvent);
|
||||
const nodeFetchFetcher = new NodeFetchFetcher(envService, reportEvent, undefined, this._resolveNodeFetchCacheOptions(configurationService));
|
||||
if (useNodeFetchFetcher) {
|
||||
this._logService.info(`Using the Node fetch fetcher.`);
|
||||
fetchers.unshift(nodeFetchFetcher);
|
||||
@@ -125,6 +128,23 @@ export class FetcherService extends Disposable implements IFetcherService {
|
||||
return fetchers;
|
||||
}
|
||||
|
||||
private _resolveNodeFetchCacheOptions(configurationService: IConfigurationService): NodeFetchCacheOptions {
|
||||
const mode = configurationService.getConfig(ConfigKey.Shared.DebugNodeFetchCache) as NodeFetchCacheMode;
|
||||
if (mode === 'off') {
|
||||
return { mode: 'off' };
|
||||
}
|
||||
if (mode === 'persistent') {
|
||||
const storageUri = this._extensionContext.globalStorageUri;
|
||||
if (storageUri && storageUri.scheme === 'file') {
|
||||
return {
|
||||
mode: 'persistent',
|
||||
storeLocation: path.join(storageUri.fsPath, 'undici-cache.v1.sqlite'),
|
||||
};
|
||||
}
|
||||
}
|
||||
return { mode: 'memory' };
|
||||
}
|
||||
|
||||
getUserAgentLibrary(): string {
|
||||
return this._getAvailableFetchers()[0].getUserAgentLibrary();
|
||||
}
|
||||
@@ -154,6 +174,7 @@ export class FetcherService extends Disposable implements IFetcherService {
|
||||
latencyMs: Date.now() - start,
|
||||
statusCode: res.status,
|
||||
success: res.ok,
|
||||
cacheStatus: res.cacheStatus,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('FetcherService network process crash handling', () => {
|
||||
logService,
|
||||
{ machineId: '', sessionId: '', vscodeVersion: '', getName: () => 'test', getVersion: () => '0.0.0', getBuildType: () => 'development' as any } as any,
|
||||
configurationService,
|
||||
{ globalStorageUri: undefined } as any,
|
||||
);
|
||||
service.setExperimentationService(experimentationService);
|
||||
// Inject the fetchers directly
|
||||
@@ -215,6 +216,7 @@ describe('FetcherService network process crash handling', () => {
|
||||
logService,
|
||||
{ machineId: '', sessionId: '', vscodeVersion: '', getName: () => 'test', getVersion: () => '0.0.0', getBuildType: () => 'development' as any } as any,
|
||||
configurationService,
|
||||
{ globalStorageUri: undefined } as any,
|
||||
);
|
||||
(service as any)._availableFetchers = [electronFetcher, nodeFetcher];
|
||||
// Explicitly do NOT call service.setExperimentationService()
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as http from 'http';
|
||||
import { IFetcherService } from '../../common/fetcherService';
|
||||
import { createExtensionTestingServices } from '../../../../extension/test/vscode-node/services';
|
||||
import { ITestingServicesAccessor } from '../../../test/node/services';
|
||||
|
||||
suite('NodeFetchFetcher cache - integration', function () {
|
||||
let server: http.Server;
|
||||
let origin: string;
|
||||
let originHits: number;
|
||||
let accessor: ITestingServicesAccessor;
|
||||
let fetcher: IFetcherService;
|
||||
|
||||
suiteSetup(async function () {
|
||||
if (typeof (globalThis as { __vscodeCreateFetchPatch?: unknown }).__vscodeCreateFetchPatch !== 'function') {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
server = http.createServer((req, res) => {
|
||||
originHits++;
|
||||
const url = req.url ?? '';
|
||||
if (url.startsWith('/cacheable')) {
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'public, max-age=60',
|
||||
'x-custom-header': 'preserved',
|
||||
});
|
||||
res.end(JSON.stringify({ hits: originHits, path: url }));
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/private')) {
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'private, max-age=60',
|
||||
});
|
||||
res.end(JSON.stringify({ hits: originHits }));
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/etag')) {
|
||||
if (req.headers['if-none-match'] === '"v1"') {
|
||||
res.writeHead(304, { 'etag': '"v1"' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'public, max-age=60',
|
||||
'etag': '"v1"',
|
||||
});
|
||||
res.end(JSON.stringify({ hits: originHits }));
|
||||
return;
|
||||
}
|
||||
if (url.startsWith('/no-store')) {
|
||||
res.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'cache-control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ hits: originHits }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const port = (server.address() as { port: number }).port;
|
||||
origin = `http://127.0.0.1:${port}`;
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
setup(() => {
|
||||
originHits = 0;
|
||||
accessor = createExtensionTestingServices().createTestingAccessor();
|
||||
fetcher = accessor.get(IFetcherService);
|
||||
});
|
||||
|
||||
test('serves the second cache-opted request from the in-memory store', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/cacheable?key=hit-miss`, { callSite: 'test', cache: true });
|
||||
const firstBody = await first.json();
|
||||
const second = await fetcher.fetch(`${origin}/cacheable?key=hit-miss`, { callSite: 'test', cache: true });
|
||||
const secondBody = await second.json();
|
||||
|
||||
assert.strictEqual(originHits, 1, 'origin should be hit exactly once');
|
||||
assert.strictEqual(first.cacheStatus, 'miss');
|
||||
assert.strictEqual(second.cacheStatus, 'hit');
|
||||
assert.deepStrictEqual(secondBody, firstBody, 'cached body should match origin body');
|
||||
assert.notStrictEqual(second.headers.get('age'), null, 'cache hit should carry age header');
|
||||
});
|
||||
|
||||
test('leaves requests uncached when the caller does not opt in', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/cacheable?key=no-opt-in`, { callSite: 'test' });
|
||||
await first.text();
|
||||
const second = await fetcher.fetch(`${origin}/cacheable?key=no-opt-in`, { callSite: 'test' });
|
||||
await second.text();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'every request should hit the origin');
|
||||
assert.strictEqual(first.cacheStatus, undefined);
|
||||
assert.strictEqual(second.cacheStatus, undefined);
|
||||
});
|
||||
|
||||
test('respects cache-control: no-store from the origin', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/no-store?key=no-store`, { callSite: 'test', cache: true });
|
||||
await first.text();
|
||||
const second = await fetcher.fetch(`${origin}/no-store?key=no-store`, { callSite: 'test', cache: true });
|
||||
await second.text();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'no-store responses must not be served from cache');
|
||||
assert.strictEqual(first.cacheStatus, 'miss');
|
||||
assert.strictEqual(second.cacheStatus, 'miss');
|
||||
});
|
||||
|
||||
test('does not cache POST requests', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/cacheable?key=post`, { callSite: 'test', cache: true, method: 'POST', body: '{}' });
|
||||
await first.text();
|
||||
const second = await fetcher.fetch(`${origin}/cacheable?key=post`, { callSite: 'test', cache: true, method: 'POST', body: '{}' });
|
||||
await second.text();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'POST requests must always reach the origin');
|
||||
assert.strictEqual(first.cacheStatus, 'miss');
|
||||
assert.strictEqual(second.cacheStatus, 'miss');
|
||||
});
|
||||
|
||||
test('caches different URLs independently', async () => {
|
||||
const a1 = await fetcher.fetch(`${origin}/cacheable?key=urlA`, { callSite: 'test', cache: true });
|
||||
await a1.text();
|
||||
const b1 = await fetcher.fetch(`${origin}/cacheable?key=urlB`, { callSite: 'test', cache: true });
|
||||
await b1.text();
|
||||
const a2 = await fetcher.fetch(`${origin}/cacheable?key=urlA`, { callSite: 'test', cache: true });
|
||||
await a2.text();
|
||||
const b2 = await fetcher.fetch(`${origin}/cacheable?key=urlB`, { callSite: 'test', cache: true });
|
||||
await b2.text();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'each distinct URL should hit the origin once');
|
||||
assert.strictEqual(a1.cacheStatus, 'miss');
|
||||
assert.strictEqual(b1.cacheStatus, 'miss');
|
||||
assert.strictEqual(a2.cacheStatus, 'hit');
|
||||
assert.strictEqual(b2.cacheStatus, 'hit');
|
||||
});
|
||||
|
||||
test('keys cache entries by query string', async () => {
|
||||
const v1 = await fetcher.fetch(`${origin}/cacheable?v=1`, { callSite: 'test', cache: true });
|
||||
await v1.text();
|
||||
const v2 = await fetcher.fetch(`${origin}/cacheable?v=2`, { callSite: 'test', cache: true });
|
||||
await v2.text();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'different query strings should not collide');
|
||||
assert.strictEqual(v1.cacheStatus, 'miss');
|
||||
assert.strictEqual(v2.cacheStatus, 'miss');
|
||||
});
|
||||
|
||||
test('preserves status, body and response headers on a cache hit', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/cacheable?key=headers`, { callSite: 'test', cache: true });
|
||||
const firstBody = await first.json();
|
||||
const second = await fetcher.fetch(`${origin}/cacheable?key=headers`, { callSite: 'test', cache: true });
|
||||
const secondBody = await second.json();
|
||||
|
||||
assert.strictEqual(originHits, 1);
|
||||
assert.strictEqual(second.status, first.status);
|
||||
assert.strictEqual(second.statusText, first.statusText);
|
||||
assert.strictEqual(second.headers.get('content-type'), 'application/json');
|
||||
assert.strictEqual(second.headers.get('x-custom-header'), 'preserved');
|
||||
assert.deepStrictEqual(secondBody, firstBody);
|
||||
});
|
||||
|
||||
test('caches Cache-Control: private responses in the private store', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/private?key=private`, { callSite: 'test', cache: true });
|
||||
await first.text();
|
||||
const second = await fetcher.fetch(`${origin}/private?key=private`, { callSite: 'test', cache: true });
|
||||
await second.text();
|
||||
|
||||
assert.strictEqual(originHits, 1, 'private responses should be cached in the private store');
|
||||
assert.strictEqual(first.cacheStatus, 'miss');
|
||||
assert.strictEqual(second.cacheStatus, 'hit');
|
||||
});
|
||||
|
||||
test('reports revalidated when the origin returns 304 Not Modified', async () => {
|
||||
const first = await fetcher.fetch(`${origin}/etag?key=etag`, { callSite: 'test', cache: true });
|
||||
const firstBody = await first.json();
|
||||
const second = await fetcher.fetch(`${origin}/etag?key=etag`, { callSite: 'test', cache: true, headers: { 'cache-control': 'no-cache' } });
|
||||
const secondBody = await second.json();
|
||||
|
||||
assert.strictEqual(originHits, 2, 'a 304 still counts as an origin request');
|
||||
assert.strictEqual(first.cacheStatus, 'miss');
|
||||
assert.strictEqual(second.cacheStatus, 'revalidated');
|
||||
assert.strictEqual(second.status, 200, 'revalidated response is served with the cached 200 status');
|
||||
assert.deepStrictEqual(secondBody, firstBody, 'revalidated response carries the cached body');
|
||||
});
|
||||
|
||||
test('coalesces or caches concurrent requests so the second is not an extra miss', async () => {
|
||||
const [a, b] = await Promise.all([
|
||||
fetcher.fetch(`${origin}/cacheable?key=concurrent`, { callSite: 'test', cache: true }),
|
||||
fetcher.fetch(`${origin}/cacheable?key=concurrent`, { callSite: 'test', cache: true }),
|
||||
]);
|
||||
await Promise.all([a.text(), b.text()]);
|
||||
const third = await fetcher.fetch(`${origin}/cacheable?key=concurrent`, { callSite: 'test', cache: true });
|
||||
await third.text();
|
||||
assert.ok(originHits <= 2, `origin hit at most twice for concurrent fetches, got ${originHits}`);
|
||||
assert.strictEqual(third.cacheStatus, 'hit');
|
||||
});
|
||||
});
|
||||
@@ -89,12 +89,14 @@ export class TelemetryService extends BaseTelemetryService {
|
||||
"owner": "lramos15",
|
||||
"comment": "Telemetry about fetch requests made by the extension, tracking request counts and latency per call site.",
|
||||
"callSite": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The call site identifier for the fetch request." },
|
||||
"cacheStatus": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Cache outcome for callers that opted in: 'hit', 'stale-hit', 'revalidated', 'miss', 'bypass'. Empty string when caching was not requested." },
|
||||
"latencyMs": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "The latency of the fetch request in milliseconds." },
|
||||
"statusCode": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "The HTTP status code returned by the fetch request." }
|
||||
}
|
||||
*/
|
||||
this.sendMSFTTelemetryEvent('fetchTelemetry', {
|
||||
callSite: new TelemetryTrustedValue(event.callSite),
|
||||
cacheStatus: event.cacheStatus ?? '',
|
||||
}, {
|
||||
latencyMs: event.latencyMs,
|
||||
statusCode: event.statusCode,
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/policy-watcher": "^1.3.2",
|
||||
"@vscode/proxy-agent": "^0.41.0",
|
||||
"@vscode/proxy-agent": "^0.42.0",
|
||||
"@vscode/ripgrep-universal": "^1.18.0",
|
||||
"@vscode/sandbox-runtime": "0.0.1",
|
||||
"@vscode/spdlog": "^0.15.8",
|
||||
@@ -4044,9 +4044,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/proxy-agent": {
|
||||
"version": "0.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.41.0.tgz",
|
||||
"integrity": "sha512-xdjSPUu6DyC7+RBRftrj06OBG/xVLc0dsxhhwMzwfd9/pOGm8j4Zc70arq1jQb0s7EF4m9dAFoNjmSigfzN25A==",
|
||||
"version": "0.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.42.0.tgz",
|
||||
"integrity": "sha512-uFEBHiWPtBdbn+BFBVzyCMqqhdxRaRdPawLen1JZ+zM8pdKHsrVO+smmo/PbM6HgHr+MKGezDmxZ9cEHv49gEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "^3.0.0",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/policy-watcher": "^1.3.2",
|
||||
"@vscode/proxy-agent": "^0.41.0",
|
||||
"@vscode/proxy-agent": "^0.42.0",
|
||||
"@vscode/ripgrep-universal": "^1.18.0",
|
||||
"@vscode/sandbox-runtime": "0.0.1",
|
||||
"@vscode/spdlog": "^0.15.8",
|
||||
|
||||
8
remote/package-lock.json
generated
8
remote/package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.41.0",
|
||||
"@vscode/proxy-agent": "^0.42.0",
|
||||
"@vscode/ripgrep-universal": "^1.18.0",
|
||||
"@vscode/sandbox-runtime": "0.0.1",
|
||||
"@vscode/spdlog": "^0.15.8",
|
||||
@@ -586,9 +586,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/proxy-agent": {
|
||||
"version": "0.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.41.0.tgz",
|
||||
"integrity": "sha512-xdjSPUu6DyC7+RBRftrj06OBG/xVLc0dsxhhwMzwfd9/pOGm8j4Zc70arq1jQb0s7EF4m9dAFoNjmSigfzN25A==",
|
||||
"version": "0.42.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.42.0.tgz",
|
||||
"integrity": "sha512-uFEBHiWPtBdbn+BFBVzyCMqqhdxRaRdPawLen1JZ+zM8pdKHsrVO+smmo/PbM6HgHr+MKGezDmxZ9cEHv49gEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/once": "^3.0.0",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/native-watchdog": "^1.4.6",
|
||||
"@vscode/proxy-agent": "^0.41.0",
|
||||
"@vscode/proxy-agent": "^0.42.0",
|
||||
"@vscode/ripgrep-universal": "^1.18.0",
|
||||
"@vscode/sandbox-runtime": "0.0.1",
|
||||
"@vscode/spdlog": "^0.15.8",
|
||||
|
||||
@@ -142,9 +142,12 @@ function patchGlobalFetch(params: ProxyAgentParams, configProvider: ExtHostConfi
|
||||
const originalFetch = globalThis.fetch;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
(globalThis as any).__vscodeOriginalFetch = originalFetch;
|
||||
const patchedFetch = proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL);
|
||||
const createPatchedFetch = (options?: proxyAgent.CreateFetchPatchOptions) => proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL, options);
|
||||
const patchedFetch = createPatchedFetch();
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
(globalThis as any).__vscodePatchedFetch = patchedFetch;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
(globalThis as any).__vscodeCreateFetchPatch = createPatchedFetch;
|
||||
let useElectronFetch = false;
|
||||
if (!initData.remote.isRemote) {
|
||||
useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);
|
||||
|
||||
Reference in New Issue
Block a user