Path: blob/main/extensions/copilot/src/platform/networking/node/baseFetchFetcher.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { generateUuid } from '../../../util/vs/base/common/uuid';7import { IEnvService } from '../../env/common/envService';8import { collectSingleLineErrorMessage } from '../../log/common/logService';9import { FetcherId, FetchOptions, IAbortController, isAbortError, PaginationOptions, ReportFetchEvent, Response, safeGetHostname } from '../common/fetcherService';10import { IFetcher, userAgentLibraryHeader } from '../common/networking';1112export abstract class BaseFetchFetcher implements IFetcher {1314constructor(15private readonly _fetchImpl: typeof fetch | typeof import('electron').net.fetch,16private readonly _envService: IEnvService,17private readonly _fetcherId: FetcherId,18private readonly _reportEvent: ReportFetchEvent,19private readonly userAgentLibraryUpdate?: (original: string) => string,20) {21}2223abstract getUserAgentLibrary(): string;2425async fetch(url: string, options: FetchOptions): Promise<Response> {26const headers = { ...options.headers };27if (!headers['User-Agent']) {28headers['User-Agent'] = `GitHubCopilotChat/${this._envService.getVersion()}`;29}30headers[userAgentLibraryHeader] = this.userAgentLibraryUpdate ? this.userAgentLibraryUpdate(this.getUserAgentLibrary()) : this.getUserAgentLibrary();3132let body = options.body;33if (options.json) {34if (options.body) {35throw new Error(`Illegal arguments! Cannot pass in both 'body' and 'json'!`);36}37headers['Content-Type'] = 'application/json';38body = JSON.stringify(options.json);39}4041const method = options.method || 'GET';42if (method !== 'GET' && method !== 'POST' && method !== 'PUT') {43throw new Error(`Illegal arguments! 'method' must be 'GET', 'POST', or 'PUT'!`);44}4546const signal = options.signal ?? new AbortController().signal;47if (signal && !(signal instanceof AbortSignal)) {48throw new Error(`Illegal arguments! 'signal' must be an instance of AbortSignal!`);49}5051const internalId = generateUuid();52const hostname = safeGetHostname(url);53try {54const response = await this._fetch(url, method, headers, body, signal, internalId, hostname);55this._reportEvent({ internalId, timestamp: Date.now(), outcome: 'success', phase: 'requestResponse', fetcher: this._fetcherId, hostname, statusCode: response.status });56return response;57} catch (e) {58e.fetcherId = this._fetcherId;59const outcome = e && !isAbortError(e) ? 'error' as const : 'cancel' as const;60this._reportEvent({ internalId, timestamp: Date.now(), outcome, phase: 'requestResponse', fetcher: this._fetcherId, hostname, reason: e });61throw e;62}63}6465async fetchWithPagination<T>(baseUrl: string, options: PaginationOptions<T>): Promise<T[]> {66const items: T[] = [];67const pageSize = options.pageSize ?? 20;68let page = options.startPage ?? 1;69let hasNextPage = false;7071do {72const url = options.buildUrl(baseUrl, pageSize, page);73const response = await this.fetch(url, options);7475if (!response.ok) {76// Return what we've collected so far if request fails77return items;78}7980const data = await response.json();81const pageItems = options.getItemsFromResponse(data);82items.push(...pageItems);8384hasNextPage = pageItems.length === pageSize;85page++;86} while (hasNextPage);8788return items;89}9091private async _fetch(url: string, method: 'GET' | 'POST' | 'PUT', headers: { [name: string]: string }, body: string | undefined, signal: AbortSignal, internalId: string, hostname: string): Promise<Response> {92const resp = await this._fetchImpl(url, { method, headers, body, signal });93return new Response(94resp.status,95resp.statusText,96resp.headers,97resp.body,98this._fetcherId,99this._reportEvent,100internalId,101hostname,102);103}104105async disconnectAll(): Promise<void> {106// Nothing to do107}108makeAbortController(): IAbortController {109return new AbortController();110}111isAbortError(e: any): boolean {112// see https://github.com/nodejs/node/issues/38361#issuecomment-1683839467113return e && e.name === 'AbortError';114}115abstract isInternetDisconnectedError(e: any): boolean;116abstract isFetcherError(e: any): boolean;117isNetworkProcessCrashedError(_e: any): boolean {118return false;119}120getUserMessageForFetcherError(err: any): string {121return `Please check your firewall rules and network connection then try again. Error Code: ${collectSingleLineErrorMessage(err)}.`;122}123}124125126