Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/github-authentication/src/node/fetch.ts
5221 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
import * as http from 'http';
7
import * as https from 'https';
8
import { workspace } from 'vscode';
9
import { Log } from '../common/logger';
10
import { Readable } from 'stream';
11
12
export interface FetchOptions {
13
logger: Log;
14
retryFallbacks: boolean;
15
expectJSON: boolean;
16
method?: 'GET' | 'POST' | 'DELETE';
17
headers?: Record<string, string>;
18
body?: string;
19
signal?: AbortSignal;
20
}
21
22
export interface FetchHeaders {
23
get(name: string): string | null;
24
}
25
26
export interface FetchResponse {
27
ok: boolean;
28
status: number;
29
statusText: string;
30
headers: FetchHeaders;
31
text(): Promise<string>;
32
json(): Promise<any>;
33
}
34
35
export type Fetch = (url: string, options: FetchOptions) => Promise<FetchResponse>;
36
37
interface Fetcher {
38
name: string;
39
fetch: Fetch;
40
}
41
42
const _fetchers: Fetcher[] = [];
43
try {
44
_fetchers.push({
45
name: 'Electron fetch',
46
fetch: require('electron').net.fetch
47
});
48
} catch {
49
// ignore
50
}
51
52
const nodeFetch = {
53
name: 'Node fetch',
54
fetch,
55
};
56
const useElectronFetch = workspace.getConfiguration('github-authentication').get<boolean>('useElectronFetch', true);
57
if (useElectronFetch) {
58
_fetchers.push(nodeFetch);
59
} else {
60
_fetchers.unshift(nodeFetch);
61
}
62
63
_fetchers.push({
64
name: 'Node http/s',
65
fetch: nodeHTTP,
66
});
67
68
export function createFetch(): Fetch {
69
let fetchers: readonly Fetcher[] = _fetchers;
70
return async (url, options) => {
71
const result = await fetchWithFallbacks(fetchers, url, options, options.logger);
72
if (result.updatedFetchers) {
73
fetchers = result.updatedFetchers;
74
}
75
return result.response;
76
};
77
}
78
79
function shouldNotRetry(status: number): boolean {
80
// Don't retry with other fetchers for these HTTP status codes:
81
// - 429 Too Many Requests (rate limiting)
82
// - 401 Unauthorized (authentication issue)
83
// - 403 Forbidden (authorization issue)
84
// - 404 Not Found (resource doesn't exist)
85
// These are application-level errors where retrying with a different fetcher won't help
86
return status === 429 || status === 401 || status === 403 || status === 404;
87
}
88
89
async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: string, options: FetchOptions, logService: Log): Promise<{ response: FetchResponse; updatedFetchers?: Fetcher[] }> {
90
if (options.retryFallbacks && availableFetchers.length > 1) {
91
let firstResult: { ok: boolean; response: FetchResponse } | { ok: false; err: any } | undefined;
92
for (const fetcher of availableFetchers) {
93
const result = await tryFetch(fetcher, url, options, logService);
94
if (fetcher === availableFetchers[0]) {
95
firstResult = result;
96
}
97
if (!result.ok) {
98
// For certain HTTP status codes, don't retry with other fetchers
99
// These are application-level errors, not network-level errors
100
if ('response' in result && shouldNotRetry(result.response.status)) {
101
return { response: result.response };
102
}
103
continue;
104
}
105
if (fetcher !== availableFetchers[0]) {
106
const retry = await tryFetch(availableFetchers[0], url, options, logService);
107
if (retry.ok) {
108
return { response: retry.response };
109
}
110
logService.info(`FetcherService: using ${fetcher.name} from now on`);
111
const updatedFetchers = availableFetchers.slice();
112
updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);
113
updatedFetchers.unshift(fetcher);
114
return { response: result.response, updatedFetchers };
115
}
116
return { response: result.response };
117
}
118
if ('response' in firstResult!) {
119
return { response: firstResult.response };
120
}
121
throw firstResult!.err;
122
}
123
return { response: await availableFetchers[0].fetch(url, options) };
124
}
125
126
async function tryFetch(fetcher: Fetcher, url: string, options: FetchOptions, logService: Log): Promise<{ ok: boolean; response: FetchResponse } | { ok: false; err: any }> {
127
try {
128
logService.debug(`FetcherService: trying fetcher ${fetcher.name} for ${url}`);
129
const response = await fetcher.fetch(url, options);
130
if (!response.ok) {
131
logService.info(`FetcherService: ${fetcher.name} failed with status: ${response.status} ${response.statusText}`);
132
return { ok: false, response };
133
}
134
if (!options.expectJSON) {
135
logService.debug(`FetcherService: ${fetcher.name} succeeded (not JSON)`);
136
return { ok: response.ok, response };
137
}
138
const text = await response.text();
139
try {
140
const json = JSON.parse(text); // Verify JSON
141
logService.debug(`FetcherService: ${fetcher.name} succeeded (JSON)`);
142
return { ok: true, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => json, async () => Readable.from([text])) };
143
} catch (err) {
144
logService.info(`FetcherService: ${fetcher.name} failed to parse JSON: ${err.message}`);
145
return { ok: false, err, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => { throw err; }, async () => Readable.from([text])) };
146
}
147
} catch (err) {
148
logService.info(`FetcherService: ${fetcher.name} failed with error: ${err.message}`);
149
return { ok: false, err };
150
}
151
}
152
153
export const fetching = createFetch();
154
155
class FetchResponseImpl implements FetchResponse {
156
public readonly ok: boolean;
157
constructor(
158
public readonly status: number,
159
public readonly statusText: string,
160
public readonly headers: FetchHeaders,
161
public readonly text: () => Promise<string>,
162
public readonly json: () => Promise<any>,
163
public readonly body: () => Promise<NodeJS.ReadableStream | null>,
164
) {
165
this.ok = this.status >= 200 && this.status < 300;
166
}
167
}
168
169
async function nodeHTTP(url: string, options: FetchOptions): Promise<FetchResponse> {
170
return new Promise((resolve, reject) => {
171
const { method, headers, body, signal } = options;
172
const module = url.startsWith('https:') ? https : http;
173
const req = module.request(url, { method, headers }, res => {
174
if (signal?.aborted) {
175
res.destroy();
176
req.destroy();
177
reject(makeAbortError(signal));
178
return;
179
}
180
181
const nodeFetcherResponse = new NodeFetcherResponse(req, res, signal);
182
resolve(new FetchResponseImpl(
183
res.statusCode || 0,
184
res.statusMessage || '',
185
nodeFetcherResponse.headers,
186
async () => nodeFetcherResponse.text(),
187
async () => nodeFetcherResponse.json(),
188
async () => nodeFetcherResponse.body(),
189
));
190
});
191
req.setTimeout(60 * 1000); // time out after 60s of receiving no data
192
req.on('error', reject);
193
194
if (body) {
195
req.write(body);
196
}
197
req.end();
198
});
199
}
200
201
class NodeFetcherResponse {
202
203
readonly headers: FetchHeaders;
204
205
constructor(
206
readonly req: http.ClientRequest,
207
readonly res: http.IncomingMessage,
208
readonly signal: AbortSignal | undefined,
209
) {
210
this.headers = new class implements FetchHeaders {
211
get(name: string): string | null {
212
const result = res.headers[name];
213
return Array.isArray(result) ? result[0] : result ?? null;
214
}
215
[Symbol.iterator](): Iterator<[string, string], any, undefined> {
216
const keys = Object.keys(res.headers);
217
let index = 0;
218
return {
219
next: (): IteratorResult<[string, string]> => {
220
if (index >= keys.length) {
221
return { done: true, value: undefined };
222
}
223
const key = keys[index++];
224
return { done: false, value: [key, this.get(key)!] };
225
}
226
};
227
}
228
};
229
}
230
231
public text(): Promise<string> {
232
return new Promise<string>((resolve, reject) => {
233
const chunks: Buffer[] = [];
234
this.res.on('data', chunk => chunks.push(chunk));
235
this.res.on('end', () => resolve(Buffer.concat(chunks).toString()));
236
this.res.on('error', reject);
237
this.signal?.addEventListener('abort', () => {
238
this.res.destroy();
239
this.req.destroy();
240
reject(makeAbortError(this.signal!));
241
});
242
});
243
}
244
245
public async json(): Promise<any> {
246
const text = await this.text();
247
return JSON.parse(text);
248
}
249
250
public async body(): Promise<NodeJS.ReadableStream | null> {
251
this.signal?.addEventListener('abort', () => {
252
this.res.emit('error', makeAbortError(this.signal!));
253
this.res.destroy();
254
this.req.destroy();
255
});
256
return this.res;
257
}
258
}
259
260
function makeAbortError(signal: AbortSignal): Error {
261
// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
262
return signal.reason;
263
}
264
265