Path: blob/main/extensions/copilot/src/platform/networking/node/fetcherFallback.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { Config, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';7import { collectSingleLineErrorMessage, ILogService } from '../../log/common/logService';8import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';9import { ITelemetryService } from '../../telemetry/common/telemetry';10import { FetcherId, FetchOptions, Response } from '../common/fetcherService';11import { IFetcher } from '../common/networking';121314const fetcherConfigKeys: Partial<Record<FetcherId, Config<boolean>>> = {15'electron-fetch': ConfigKey.Shared.DebugUseElectronFetcher,16'node-fetch': ConfigKey.Shared.DebugUseNodeFetchFetcher,17'node-http': ConfigKey.Shared.DebugUseNodeFetcher,18};1920export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[], url: string, options: FetchOptions, knownBadFetchers: Set<string>, configurationService: IConfigurationService, logService: ILogService, telemetryService: ITelemetryService | undefined, experimentationService: IExperimentationService | undefined): Promise<{ response: Response; updatedFetchers?: IFetcher[]; updatedKnownBadFetchers?: Set<string> }> {21if (options.retryFallbacks && availableFetchers.length > 1) {22let firstResult: { ok: boolean; response: Response } | { ok: false; err: any } | undefined;23const updatedKnownBadFetchers = new Set<string>();24let lastError: string | undefined;25for (const fetcher of availableFetchers) {26const result = await tryFetch(fetcher, url, options, logService);27if (fetcher === availableFetchers[0]) {28firstResult = result;29}30if (!result.ok) {31const fetcherId = fetcher.getUserAgentLibrary();32if ('response' in result) {33lastError = `${fetcherId}: ${result.response.status} ${result.response.statusText}`;34} else {35lastError = `${fetcherId}: ${collectSingleLineErrorMessage(result.err, true)}`;36}37updatedKnownBadFetchers.add(fetcherId);38continue;39}40if (fetcher !== availableFetchers[0]) {41const retry = await tryFetch(availableFetchers[0], url, options, logService);42if (retry.ok) {43return { response: retry.response };44}45logService.info(`FetcherService: using ${fetcher.getUserAgentLibrary()} from now on`);46/* __GDPR__47"fetcherFallback" : {48"owner": "chrmarti",49"comment": "Sent when the fetcher service switches to a fallback fetcher due to the primary fetcher failing",50"newFetcher": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The name of the fetcher that is now being used" },51"knownBadFetchers": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Comma-separated list of fetchers that are known to be failing" },52"knownBadFetchersCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of fetchers that are known to be failing" },53"lastError": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The last error encountered, containing fetcher ID, status code and error message" }54}55*/56telemetryService?.sendTelemetryEvent('fetcherFallback', { github: true, microsoft: true }, {57newFetcher: fetcher.getUserAgentLibrary(),58knownBadFetchers: Array.from(updatedKnownBadFetchers).join(','),59lastError,60}, {61knownBadFetchersCount: updatedKnownBadFetchers.size,62});63const updatedFetchers = availableFetchers.slice();64updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);65updatedFetchers.unshift(fetcher);66return { response: result.response, updatedFetchers, updatedKnownBadFetchers };67}68return { response: result.response };69}70if ('response' in firstResult!) {71return { response: firstResult.response };72}73throw firstResult!.err;74}75let fetcher = availableFetchers[0];76if (options.useFetcher) {77if (knownBadFetchers.has(options.useFetcher)) {78logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is known to be failing, using ${fetcher.getUserAgentLibrary()} instead.`);79} else {80const configKey = fetcherConfigKeys[options.useFetcher];81if (configKey && configurationService.inspectConfig(configKey)?.globalValue === false) {82logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is disabled in user settings, using ${fetcher.getUserAgentLibrary()} instead.`);83} else {84const requestedFetcher = availableFetchers.find(f => f.getUserAgentLibrary() === options.useFetcher);85if (requestedFetcher) {86fetcher = requestedFetcher;87logService.trace(`FetcherService: using ${options.useFetcher} as requested.`);88} else {89logService.info(`FetcherService: could not find requested fetcher ${options.useFetcher}, using ${fetcher.getUserAgentLibrary()} instead.`);90}91}92}93}94try {95return { response: await fetcher.fetch(url, options) };96} catch (err) {97// For net::ERR_FAILED from network process crash, disconnect and retry once.98if (fetcher.isNetworkProcessCrashedError(err)) {99const fetcherId = fetcher.getUserAgentLibrary();100logService.info(`FetcherService: ${fetcherId} hit network process crash error (${(err as Error)?.message}), retrying after disconnect...`);101try {102await fetcher.disconnectAll();103const response = await fetcher.fetch(url, options);104logService.info(`FetcherService: ${fetcherId} retry after crash succeeded.`);105/* __GDPR__106"fetcherCrashRetry" : {107"owner": "deepak1556",108"comment": "Sent when a fetcher retries after a network process crash error",109"fetcher": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The fetcher that crashed" },110"outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the retry recovered or failed" },111"error": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The error message" }112}113*/114telemetryService?.sendTelemetryEvent('fetcherCrashRetry', { github: true, microsoft: true }, {115fetcher: fetcherId,116outcome: 'recovered',117error: collectSingleLineErrorMessage(err, true),118});119return { response };120} catch (retryErr) {121logService.info(`FetcherService: ${fetcherId} retry also failed (${(retryErr as Error)?.message}), checking for demotion...`);122telemetryService?.sendTelemetryEvent('fetcherCrashRetry', { github: true, microsoft: true }, {123fetcher: fetcherId,124outcome: 'failed',125error: collectSingleLineErrorMessage(retryErr, true),126});127err = retryErr;128}129}130131// When Electron's network process crashes, it's permanently dead until the extension host restarts.132// Above, we retry the current request once after disconnecting all connections if this is a crash error.133// If that retry still fails and crash fallback is enabled, demote the crashed fetcher so future requests use a healthy one.134// After demotion, the caller is responsible for deciding whether to retry or surface the error.135const enableCrashFallback = experimentationService136? configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.FallbackNodeFetchOnNetworkProcessCrash, experimentationService)137: false;138if (enableCrashFallback && fetcher.isNetworkProcessCrashedError(err)) {139const fetcherId = fetcher.getUserAgentLibrary();140logService.info(`FetcherService: ${fetcherId} network process crashed. Permanently demoting to avoid future use.`);141const updatedKnownBadFetchers = new Set(knownBadFetchers);142updatedKnownBadFetchers.add(fetcherId);143const updatedFetchers = availableFetchers.filter(f => f !== fetcher);144if (updatedFetchers.length > 0) {145updatedFetchers.push(fetcher);146logService.info(`FetcherService: now using ${updatedFetchers[0].getUserAgentLibrary()} as primary fetcher.`);147}148// Attach demotion info to the error so the caller can apply it149(err as any)._fetcherDemotion = { updatedFetchers: updatedFetchers.length > 0 ? updatedFetchers : undefined, updatedKnownBadFetchers };150}151throw err;152}153}154155async function tryFetch(fetcher: IFetcher, url: string, options: FetchOptions, logService: ILogService): Promise<{ ok: boolean; response: Response } | { ok: false; err: any }> {156try {157const response = await fetcher.fetch(url, options);158if (!response.ok) {159logService.info(`FetcherService: ${fetcher.getUserAgentLibrary()} failed with status: ${response.status} ${response.statusText}`);160return { ok: false, response };161}162if (!options.expectJSON) {163logService.debug(`FetcherService: ${fetcher.getUserAgentLibrary()} succeeded (not JSON)`);164return { ok: response.ok, response };165}166const text = await response.text();167try {168JSON.parse(text); // Verify JSON169logService.debug(`FetcherService: ${fetcher.getUserAgentLibrary()} succeeded (JSON)`);170return { ok: true, response: Response.fromText(response.status, response.statusText, response.headers, text, response.fetcher) };171} catch (err) {172logService.info(`FetcherService: ${fetcher.getUserAgentLibrary()} failed to parse JSON: ${err.message}`);173return { ok: false, err, response: Response.fromText(response.status, response.statusText, response.headers, text, response.fetcher) };174}175} catch (err) {176logService.info(`FetcherService: ${fetcher.getUserAgentLibrary()} failed with error: ${err.message}`);177return { ok: false, err };178}179}180181182