// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { Readable } from "stream"; import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { CancellationToken } from "vscode"; import ILogger from "../logging/ILogger"; import { RetryUtility } from "../utility/RetryUtility"; import IHttpRequestHandler from "./IHttpRequestHandler"; export default class HttpRequestHandler implements IHttpRequestHandler { public constructor(private readonly _logger: ILogger) { } public async get( url: string, timeoutInMs: number, retries: number, retryDelayInMs: number, cancellationToken?: CancellationToken, onDownloadProgressChange?: (downloadedBytes: number, totalBytes: number) => void ): Promise<Readable> { const requestFn = () => this.getRequestHelper( url, timeoutInMs, cancellationToken, onDownloadProgressChange ); const errorHandlerFn = (error: Error) => { const statusCode = (error as any)?.response?.status; if (statusCode != null && 400 <= statusCode && statusCode < 500) { throw error; } }; return RetryUtility.exponentialRetryAsync(requestFn, `HttpRequestHandler.get`, retries, retryDelayInMs, errorHandlerFn); } private async getRequestHelper( url: string, timeoutInMs: number, cancellationToken?: CancellationToken, onDownloadProgressChange?: (downloadedBytes: number, totalBytes: number) => void ): Promise<Readable> { const options: AxiosRequestConfig = { timeout: timeoutInMs, responseType: `stream`, proxy: false // Disabling axios proxy support allows VS Code proxy settings to take effect. }; if (cancellationToken != null) { const cancelToken = axios.CancelToken; const cancelTokenSource = cancelToken.source(); cancellationToken.onCancellationRequested(() => cancelTokenSource.cancel()); options.cancelToken = cancelTokenSource.token; } let response: AxiosResponse<Readable> | undefined; try { response = await axios.get(url, options); if (response === undefined) { throw new Error(`Undefined response received when downloading from '${url}'`); } } catch (error) { if (error instanceof Error) { this._logger.error(`${error.message}. Technical details: ${JSON.stringify(error)}`); } throw error; } const downloadStream: Readable = response.data; if (cancellationToken != null) { cancellationToken.onCancellationRequested(() => { downloadStream.destroy(); }); } // We should not feed of the data handler here if we are going to pipe the downloadStream later on. // Doing this forks the pipe and creates problem e.g. FILE_ENDED, PREMATURE_CLOSE // https://nodejs.org/api/stream.html#stream_choose_one_api_style // We should make this progress reporter code a transform pipe and place it between the downloadStream and the unzipper. // if (onDownloadProgressChange != null) { // const headers: { [key: string]: any } = response.headers; // const contentLength = parseInt(headers[`content-length`], 10); // const totalBytes = contentLength as number ?? undefined; // let downloadedBytes = 0; // downloadStream.on(`data`, (chunk: Buffer) => { // downloadedBytes += chunk.length; // if (onDownloadProgressChange != null) { // onDownloadProgressChange(downloadedBytes, totalBytes); // } // }); // } return downloadStream; } }