Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/networking/node/nodeFetcher.ts
13400 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 { Readable } from 'stream';
9
import { generateUuid } from '../../../util/vs/base/common/uuid';
10
import { IEnvService } from '../../env/common/envService';
11
import { collectSingleLineErrorMessage } from '../../log/common/logService';
12
import { FetchOptions, HeadersImpl, IAbortController, IHeaders, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
13
import { IFetcher, userAgentLibraryHeader } from '../common/networking';
14
15
export class NodeFetcher implements IFetcher {
16
17
static readonly ID = 'node-http' as const;
18
19
constructor(
20
private readonly _envService: IEnvService,
21
private readonly _reportEvent: ReportFetchEvent = () => { },
22
private readonly _userAgentLibraryUpdate?: (original: string) => string,
23
) {
24
}
25
26
getUserAgentLibrary(): string {
27
return NodeFetcher.ID;
28
}
29
30
async fetch(url: string, options: FetchOptions): Promise<Response> {
31
const headers = { ...options.headers };
32
if (!headers['User-Agent']) {
33
headers['User-Agent'] = `GitHubCopilotChat/${this._envService.getVersion()}`;
34
}
35
headers[userAgentLibraryHeader] = this._userAgentLibraryUpdate ? this._userAgentLibraryUpdate(this.getUserAgentLibrary()) : this.getUserAgentLibrary();
36
37
let body = options.body;
38
if (options.json) {
39
if (options.body) {
40
throw new Error(`Illegal arguments! Cannot pass in both 'body' and 'json'!`);
41
}
42
headers['Content-Type'] = 'application/json';
43
body = JSON.stringify(options.json);
44
}
45
46
const method = options.method || 'GET';
47
if (method !== 'GET' && method !== 'POST' && method !== 'PUT') {
48
throw new Error(`Illegal arguments! 'method' must be 'GET', 'POST', or 'PUT'!`);
49
}
50
51
const signal = options.signal ?? new AbortController().signal;
52
if (signal && !(signal instanceof AbortSignal)) {
53
throw new Error(`Illegal arguments! 'signal' must be an instance of AbortSignal!`);
54
}
55
56
const internalId = generateUuid();
57
const hostname = safeGetHostname(url);
58
try {
59
const response = await this._fetch(url, method, headers, body, signal, internalId, hostname);
60
this._reportEvent({ internalId, timestamp: Date.now(), outcome: 'success', phase: 'requestResponse', fetcher: NodeFetcher.ID, hostname, statusCode: response.status });
61
return response;
62
} catch (e) {
63
e.fetcherId = NodeFetcher.ID;
64
const outcome = e && !isAbortError(e) ? 'error' as const : 'cancel' as const;
65
this._reportEvent({ internalId, timestamp: Date.now(), outcome, phase: 'requestResponse', fetcher: NodeFetcher.ID, hostname, reason: e });
66
throw e;
67
}
68
}
69
70
async fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {
71
const items: T[] = [];
72
const pageSize = options.pageSize ?? 20;
73
let page = options.startPage ?? 1;
74
let hasNextPage = false;
75
76
do {
77
const url = options.buildUrl(baseUrl, pageSize, page);
78
const response = await this.fetch(url, options);
79
80
if (!response.ok) {
81
// Return what we've collected so far if request fails
82
return items;
83
}
84
85
const data = await response.json();
86
const pageItems = options.getItemsFromResponse(data);
87
items.push(...pageItems);
88
89
hasNextPage = pageItems.length === pageSize;
90
page++;
91
} while (hasNextPage);
92
93
return items;
94
}
95
96
private _fetch(url: string, method: 'GET' | 'POST' | 'PUT', headers: { [name: string]: string }, body: string | undefined, signal: AbortSignal, internalId: string, hostname: string): Promise<Response> {
97
return new Promise((resolve, reject) => {
98
const module = url.startsWith('https:') ? https : http;
99
const req = module.request(url, { method, headers }, res => {
100
if (signal.aborted) {
101
res.destroy();
102
req.destroy();
103
reject(makeAbortError(signal));
104
return;
105
}
106
107
const nodeFetcherResponse = new NodeFetcherResponse(req, res, signal);
108
resolve(new Response(
109
res.statusCode || 0,
110
res.statusMessage || '',
111
nodeFetcherResponse.headers,
112
nodeFetcherResponse.body(),
113
NodeFetcher.ID,
114
this._reportEvent,
115
internalId,
116
hostname,
117
));
118
});
119
req.setTimeout(60 * 1000); // time out after 60s of receiving no data
120
req.on('error', reject);
121
122
if (body) {
123
req.write(body);
124
}
125
req.end();
126
});
127
}
128
async disconnectAll(): Promise<void> {
129
// Nothing to do
130
}
131
makeAbortController(): IAbortController {
132
return new AbortController();
133
}
134
isAbortError(e: any): boolean {
135
return isAbortError(e);
136
}
137
isInternetDisconnectedError(_e: any): boolean {
138
return false;
139
}
140
isFetcherError(e: any): boolean {
141
return e && ['EADDRINUSE', 'ECONNREFUSED', 'ECONNRESET', 'ENOTFOUND', 'EPIPE', 'ETIMEDOUT'].includes(e.code);
142
}
143
isNetworkProcessCrashedError(_e: any): boolean {
144
return false;
145
}
146
getUserMessageForFetcherError(err: any): string {
147
return `Please check your firewall rules and network connection then try again. Error Code: ${collectSingleLineErrorMessage(err)}.`;
148
}
149
}
150
151
function makeAbortError(signal: AbortSignal): Error {
152
// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
153
return signal.reason;
154
}
155
function isAbortError(e: any): boolean {
156
// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
157
return e && e.name === 'AbortError';
158
}
159
160
class NodeFetcherResponse {
161
162
readonly headers: IHeaders;
163
164
constructor(
165
readonly req: http.ClientRequest,
166
readonly res: http.IncomingMessage,
167
readonly signal: AbortSignal
168
) {
169
this.headers = new HeadersImpl(res.headers);
170
}
171
172
public text(): Promise<string> {
173
return new Promise<string>((resolve, reject) => {
174
const chunks: Buffer[] = [];
175
this.res.on('data', chunk => chunks.push(chunk));
176
this.res.on('end', () => resolve(Buffer.concat(chunks).toString()));
177
this.res.on('error', reject);
178
this.signal.addEventListener('abort', () => {
179
this.res.destroy();
180
this.req.destroy();
181
reject(makeAbortError(this.signal));
182
});
183
});
184
}
185
186
public async json(): Promise<any> {
187
const text = await this.text();
188
return JSON.parse(text);
189
}
190
191
public body(): ReadableStream<Uint8Array> {
192
this.signal.addEventListener('abort', () => {
193
this.res.emit('error', makeAbortError(this.signal));
194
this.res.destroy();
195
this.req.destroy();
196
});
197
return Readable.toWeb(this.res);
198
}
199
}
200
201