| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- // Copyright (c) Microsoft Corporation. All rights reserved.
- // Licensed under the MIT License. See License.txt in the project root for license information.
- import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from "axios";
- import { Transform, Readable } from "stream";
- import FormData from "form-data";
- import * as tough from "tough-cookie";
- import { HttpClient } from "./httpClient";
- import { HttpHeaders } from "./httpHeaders";
- import { HttpOperationResponse } from "./httpOperationResponse";
- import { RestError } from "./restError";
- import { WebResource, HttpRequestBody } from "./webResource";
- import * as tunnel from "tunnel";
- import { ProxySettings } from "./serviceClient";
- import * as http from "http";
- import * as https from "https";
- import { URLBuilder } from "./url";
- /**
- * A HttpClient implementation that uses axios to send HTTP requests.
- */
- export class AxiosHttpClient implements HttpClient {
- private readonly cookieJar = new tough.CookieJar();
- public async sendRequest(httpRequest: WebResource): Promise<HttpOperationResponse> {
- if (typeof httpRequest !== "object") {
- throw new Error("httpRequest (WebResource) cannot be null or undefined and must be of type object.");
- }
- if (httpRequest.formData) {
- const formData: any = httpRequest.formData;
- const requestForm = new FormData();
- const appendFormValue = (key: string, value: any) => {
- // value function probably returns a stream so we can provide a fresh stream on each retry
- if (typeof value === "function") {
- value = value();
- }
- if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
- requestForm.append(key, value.value, value.options);
- } else {
- requestForm.append(key, value);
- }
- };
- for (const formKey of Object.keys(formData)) {
- const formValue = formData[formKey];
- if (Array.isArray(formValue)) {
- for (let j = 0; j < formValue.length; j++) {
- appendFormValue(formKey, formValue[j]);
- }
- } else {
- appendFormValue(formKey, formValue);
- }
- }
- httpRequest.body = requestForm;
- httpRequest.formData = undefined;
- const contentType = httpRequest.headers.get("Content-Type");
- if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
- if (typeof requestForm.getBoundary === "function") {
- httpRequest.headers.set("Content-Type", `multipart/form-data; boundary=${requestForm.getBoundary()}`);
- } else {
- // browser will automatically apply a suitable content-type header
- httpRequest.headers.remove("Content-Type");
- }
- }
- }
- if (this.cookieJar && !httpRequest.headers.get("Cookie")) {
- const cookieString = await new Promise<string>((resolve, reject) => {
- this.cookieJar!.getCookieString(httpRequest.url, (err, cookie) => {
- if (err) {
- reject(err);
- } else {
- resolve(cookie);
- }
- });
- });
- httpRequest.headers.set("Cookie", cookieString);
- }
- const abortSignal = httpRequest.abortSignal;
- if (abortSignal && abortSignal.aborted) {
- throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest);
- }
- let abortListener: (() => void) | undefined;
- const cancelToken = abortSignal && new axios.CancelToken(canceler => {
- abortListener = () => canceler();
- abortSignal.addEventListener("abort", abortListener);
- });
- const rawHeaders: { [headerName: string]: string } = httpRequest.headers.rawHeaders();
- const httpRequestBody: HttpRequestBody = httpRequest.body;
- let axiosBody =
- // Workaround for https://github.com/axios/axios/issues/755
- // tslint:disable-next-line:no-null-keyword
- typeof httpRequestBody === "undefined" ? null :
- typeof httpRequestBody === "function" ? httpRequestBody() :
- httpRequestBody;
- const onUploadProgress = httpRequest.onUploadProgress;
- if (onUploadProgress && axiosBody) {
- let loadedBytes = 0;
- const uploadReportStream = new Transform({
- transform: (chunk: string | Buffer, _encoding, callback) => {
- loadedBytes += chunk.length;
- onUploadProgress({ loadedBytes });
- callback(undefined, chunk);
- }
- });
- if (isReadableStream(axiosBody)) {
- axiosBody.pipe(uploadReportStream);
- } else {
- uploadReportStream.end(axiosBody);
- }
- axiosBody = uploadReportStream;
- }
- let res: AxiosResponse;
- try {
- const config: AxiosRequestConfig = {
- method: httpRequest.method as Method,
- url: httpRequest.url,
- headers: rawHeaders,
- data: axiosBody,
- transformResponse: (data) => { return data; },
- validateStatus: () => true,
- // Workaround for https://github.com/axios/axios/issues/1362
- maxContentLength: Infinity,
- responseType: httpRequest.streamResponseBody ? "stream" : "text",
- cancelToken,
- timeout: httpRequest.timeout,
- proxy: false
- };
- if (httpRequest.proxySettings) {
- const agent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
- if (agent.isHttps) {
- config.httpsAgent = agent.agent;
- } else {
- config.httpAgent = agent.agent;
- }
- }
- // This hack is still required with 0.19.0 version of axios since axios tries to merge the
- // Content-Type header from it's config["<method name>"] where the method name is lower-case,
- // into the request header. It could be possible that the Content-Type header is not present
- // in the original request and this would create problems while creating the signature for
- // storage data plane sdks.
- axios.interceptors.request.use((config: AxiosRequestConfig) => ({
- ...config,
- method: (config.method as Method) && (config.method as Method).toUpperCase() as Method
- }));
- res = await axios.request(config);
- } catch (err) {
- if (err instanceof axios.Cancel) {
- throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
- } else {
- const axiosErr = err as AxiosError;
- throw new RestError(axiosErr.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
- }
- } finally {
- if (abortSignal && abortListener) {
- abortSignal.removeEventListener("abort", abortListener);
- }
- }
- const headers = new HttpHeaders(res.headers);
- const onDownloadProgress = httpRequest.onDownloadProgress;
- let responseBody: Readable | string = res.data;
- if (onDownloadProgress) {
- if (isReadableStream(responseBody)) {
- let loadedBytes = 0;
- const downloadReportStream = new Transform({
- transform: (chunk: string | Buffer, _encoding, callback) => {
- loadedBytes += chunk.length;
- onDownloadProgress({ loadedBytes });
- callback(undefined, chunk);
- }
- });
- responseBody.pipe(downloadReportStream);
- responseBody = downloadReportStream;
- } else {
- const length = parseInt(headers.get("Content-Length")!) || (responseBody as string).length || undefined;
- if (length) {
- // Calling callback for non-stream response for consistency with browser
- onDownloadProgress({ loadedBytes: length });
- }
- }
- }
- const operationResponse: HttpOperationResponse = {
- request: httpRequest,
- status: res.status,
- headers,
- readableStreamBody: httpRequest.streamResponseBody ? responseBody as Readable : undefined,
- bodyAsText: httpRequest.streamResponseBody ? undefined : responseBody as string
- };
- if (this.cookieJar) {
- const setCookieHeader = operationResponse.headers.get("Set-Cookie");
- if (setCookieHeader != undefined) {
- await new Promise((resolve, reject) => {
- this.cookieJar!.setCookie(setCookieHeader, httpRequest.url, (err) => {
- if (err) {
- reject(err);
- } else {
- resolve();
- }
- });
- });
- }
- }
- return operationResponse;
- }
- }
- function isReadableStream(body: any): body is Readable {
- return typeof body.pipe === "function";
- }
- declare type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
- export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
- const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
- proxy: {
- host: URLBuilder.parse(proxySettings.host).getHost(),
- port: proxySettings.port,
- headers: (headers && headers.rawHeaders()) || {}
- }
- };
- if ((proxySettings.username && proxySettings.password)) {
- tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
- }
- const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
- const isRequestHttps = requestScheme.toLowerCase() === "https";
- const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
- const isProxyHttps = proxyScheme.toLowerCase() === "https";
- const proxyAgent = {
- isHttps: isRequestHttps,
- agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
- };
- return proxyAgent;
- }
- export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
- if (isRequestHttps && isProxyHttps) {
- return tunnel.httpsOverHttps(tunnelOptions);
- } else if (isRequestHttps && !isProxyHttps) {
- return tunnel.httpsOverHttp(tunnelOptions);
- } else if (!isRequestHttps && isProxyHttps) {
- return tunnel.httpOverHttps(tunnelOptions);
- } else {
- return tunnel.httpOverHttp(tunnelOptions);
- }
- }
|