Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/networking/node/baseFetchFetcher.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 { generateUuid } from '../../../util/vs/base/common/uuid';
8
import { IEnvService } from '../../env/common/envService';
9
import { collectSingleLineErrorMessage } from '../../log/common/logService';
10
import { FetcherId, FetchOptions, IAbortController, isAbortError, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';
11
import { IFetcher, userAgentLibraryHeader } from '../common/networking';
12
13
export abstract class BaseFetchFetcher implements IFetcher {
14
15
constructor(
16
private readonly _fetchImpl: typeof fetch | typeof import('electron').net.fetch,
17
private readonly _envService: IEnvService,
18
private readonly _fetcherId: FetcherId,
19
private readonly _reportEvent: ReportFetchEvent,
20
private readonly userAgentLibraryUpdate?: (original: string) => string,
21
) {
22
}
23
24
abstract getUserAgentLibrary(): string;
25
26
async fetch(url: string, options: FetchOptions): Promise<Response> {
27
const headers = { ...options.headers };
28
if (!headers['User-Agent']) {
29
headers['User-Agent'] = `GitHubCopilotChat/${this._envService.getVersion()}`;
30
}
31
headers[userAgentLibraryHeader] = this.userAgentLibraryUpdate ? this.userAgentLibraryUpdate(this.getUserAgentLibrary()) : this.getUserAgentLibrary();
32
33
let body = options.body;
34
if (options.json) {
35
if (options.body) {
36
throw new Error(`Illegal arguments! Cannot pass in both 'body' and 'json'!`);
37
}
38
headers['Content-Type'] = 'application/json';
39
body = JSON.stringify(options.json);
40
}
41
42
const method = options.method || 'GET';
43
if (method !== 'GET' && method !== 'POST' && method !== 'PUT') {
44
throw new Error(`Illegal arguments! 'method' must be 'GET', 'POST', or 'PUT'!`);
45
}
46
47
const signal = options.signal ?? new AbortController().signal;
48
if (signal && !(signal instanceof AbortSignal)) {
49
throw new Error(`Illegal arguments! 'signal' must be an instance of AbortSignal!`);
50
}
51
52
const internalId = generateUuid();
53
const hostname = safeGetHostname(url);
54
try {
55
const response = await this._fetch(url, method, headers, body, signal, internalId, hostname);
56
this._reportEvent({ internalId, timestamp: Date.now(), outcome: 'success', phase: 'requestResponse', fetcher: this._fetcherId, hostname, statusCode: response.status });
57
return response;
58
} catch (e) {
59
e.fetcherId = this._fetcherId;
60
const outcome = e && !isAbortError(e) ? 'error' as const : 'cancel' as const;
61
this._reportEvent({ internalId, timestamp: Date.now(), outcome, phase: 'requestResponse', fetcher: this._fetcherId, hostname, reason: e });
62
throw e;
63
}
64
}
65
66
async fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {
67
const items: T[] = [];
68
const pageSize = options.pageSize ?? 20;
69
let page = options.startPage ?? 1;
70
let hasNextPage = false;
71
72
do {
73
const url = options.buildUrl(baseUrl, pageSize, page);
74
const response = await this.fetch(url, options);
75
76
if (!response.ok) {
77
// Return what we've collected so far if request fails
78
return items;
79
}
80
81
const data = await response.json();
82
const pageItems = options.getItemsFromResponse(data);
83
items.push(...pageItems);
84
85
hasNextPage = pageItems.length === pageSize;
86
page++;
87
} while (hasNextPage);
88
89
return items;
90
}
91
92
private async _fetch(url: string, method: 'GET' | 'POST' | 'PUT', headers: { [name: string]: string }, body: string | undefined, signal: AbortSignal, internalId: string, hostname: string): Promise<Response> {
93
const resp = await this._fetchImpl(url, { method, headers, body, signal });
94
return new Response(
95
resp.status,
96
resp.statusText,
97
resp.headers,
98
resp.body,
99
this._fetcherId,
100
this._reportEvent,
101
internalId,
102
hostname,
103
);
104
}
105
106
async disconnectAll(): Promise<void> {
107
// Nothing to do
108
}
109
makeAbortController(): IAbortController {
110
return new AbortController();
111
}
112
isAbortError(e: any): boolean {
113
// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467
114
return e && e.name === 'AbortError';
115
}
116
abstract isInternetDisconnectedError(e: any): boolean;
117
abstract isFetcherError(e: any): boolean;
118
isNetworkProcessCrashedError(_e: any): boolean {
119
return false;
120
}
121
getUserMessageForFetcherError(err: any): string {
122
return `Please check your firewall rules and network connection then try again. Error Code: ${collectSingleLineErrorMessage(err)}.`;
123
}
124
}
125
126