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