Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/github-authentication/src/node/fetch.ts
3320 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
async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: string, options: FetchOptions, logService: Log): Promise<{ response: FetchResponse; updatedFetchers?: Fetcher[] }> {
80
if (options.retryFallbacks && availableFetchers.length > 1) {
81
let firstResult: { ok: boolean; response: FetchResponse } | { ok: false; err: any } | undefined;
82
for (const fetcher of availableFetchers) {
83
const result = await tryFetch(fetcher, url, options, logService);
84
if (fetcher === availableFetchers[0]) {
85
firstResult = result;
86
}
87
if (!result.ok) {
88
continue;
89
}
90
if (fetcher !== availableFetchers[0]) {
91
const retry = await tryFetch(availableFetchers[0], url, options, logService);
92
if (retry.ok) {
93
return { response: retry.response };
94
}
95
logService.info(`FetcherService: using ${fetcher.name} from now on`);
96
const updatedFetchers = availableFetchers.slice();
97
updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);
98
updatedFetchers.unshift(fetcher);
99
return { response: result.response, updatedFetchers };
100
}
101
return { response: result.response };
102
}
103
if ('response' in firstResult!) {
104
return { response: firstResult.response };
105
}
106
throw firstResult!.err;
107
}
108
return { response: await availableFetchers[0].fetch(url, options) };
109
}
110
111
async function tryFetch(fetcher: Fetcher, url: string, options: FetchOptions, logService: Log): Promise<{ ok: boolean; response: FetchResponse } | { ok: false; err: any }> {
112
try {
113
const response = await fetcher.fetch(url, options);
114
if (!response.ok) {
115
logService.info(`FetcherService: ${fetcher.name} failed with status: ${response.status} ${response.statusText}`);
116
return { ok: false, response };
117
}
118
if (!options.expectJSON) {
119
logService.debug(`FetcherService: ${fetcher.name} succeeded (not JSON)`);
120
return { ok: response.ok, response };
121
}
122
const text = await response.text();
123
try {
124
const json = JSON.parse(text); // Verify JSON
125
logService.debug(`FetcherService: ${fetcher.name} succeeded (JSON)`);
126
return { ok: true, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => json, async () => Readable.from([text])) };
127
} catch (err) {
128
logService.info(`FetcherService: ${fetcher.name} failed to parse JSON: ${err.message}`);
129
return { ok: false, err, response: new FetchResponseImpl(response.status, response.statusText, response.headers, async () => text, async () => { throw err; }, async () => Readable.from([text])) };
130
}
131
} catch (err) {
132
logService.info(`FetcherService: ${fetcher.name} failed with error: ${err.message}`);
133
return { ok: false, err };
134
}
135
}
136
137
export const fetching = createFetch();
138
139
class FetchResponseImpl implements FetchResponse {
140
public readonly ok: boolean;
141
constructor(
142
public readonly status: number,
143
public readonly statusText: string,
144
public readonly headers: FetchHeaders,
145
public readonly text: () => Promise<string>,
146
public readonly json: () => Promise<any>,
147
public readonly body: () => Promise<NodeJS.ReadableStream | null>,
148
) {
149
this.ok = this.status >= 200 && this.status < 300;
150
}
151
}
152
153
async function nodeHTTP(url: string, options: FetchOptions): Promise<FetchResponse> {
154
return new Promise((resolve, reject) => {
155
const { method, headers, body, signal } = options;
156
const module = url.startsWith('https:') ? https : http;
157
const req = module.request(url, { method, headers }, res => {
158
if (signal?.aborted) {
159
res.destroy();
160
req.destroy();
161
reject(makeAbortError(signal));
162
return;
163
}
164
165
const nodeFetcherResponse = new NodeFetcherResponse(req, res, signal);
166
resolve(new FetchResponseImpl(
167
res.statusCode || 0,
168
res.statusMessage || '',
169
nodeFetcherResponse.headers,
170
async () => nodeFetcherResponse.text(),
171
async () => nodeFetcherResponse.json(),
172
async () => nodeFetcherResponse.body(),
173
));
174
});
175
req.setTimeout(60 * 1000); // time out after 60s of receiving no data
176
req.on('error', reject);
177
178
if (body) {
179
req.write(body);
180
}
181
req.end();
182
});
183
}
184
185
class NodeFetcherResponse {
186
187
readonly headers: FetchHeaders;
188
189
constructor(
190
readonly req: http.ClientRequest,
191
readonly res: http.IncomingMessage,
192
readonly signal: AbortSignal | undefined,
193
) {
194
this.headers = new class implements FetchHeaders {
195
get(name: string): string | null {
196
const result = res.headers[name];
197
return Array.isArray(result) ? result[0] : result ?? null;
198
}
199
[Symbol.iterator](): Iterator<[string, string], any, undefined> {
200
const keys = Object.keys(res.headers);
201
let index = 0;
202
return {
203
next: (): IteratorResult<[string, string]> => {
204
if (index >= keys.length) {
205
return { done: true, value: undefined };
206
}
207
const key = keys[index++];
208
return { done: false, value: [key, this.get(key)!] };
209
}
210
};
211
}
212
};
213
}
214
215
public text(): Promise<string> {
216
return new Promise<string>((resolve, reject) => {
217
const chunks: Buffer[] = [];
218
this.res.on('data', chunk => chunks.push(chunk));
219
this.res.on('end', () => resolve(Buffer.concat(chunks).toString()));
220
this.res.on('error', reject);
221
this.signal?.addEventListener('abort', () => {
222
this.res.destroy();
223
this.req.destroy();
224
reject(makeAbortError(this.signal!));
225
});
226
});
227
}
228
229
public async json(): Promise<any> {
230
const text = await this.text();
231
return JSON.parse(text);
232
}
233
234
public async body(): Promise<NodeJS.ReadableStream | null> {
235
this.signal?.addEventListener('abort', () => {
236
this.res.emit('error', makeAbortError(this.signal!));
237
this.res.destroy();
238
this.req.destroy();
239
});
240
return this.res;
241
}
242
}
243
244
function makeAbortError(signal: AbortSignal): Error {
245
// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
246
return signal.reason;
247
}
248
249