ms-rest-js/lib/axiosHttpClient.ts

204 строки
7.3 KiB
TypeScript

// 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 } 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";
const axiosClient = axios.create();
// Workaround for https://github.com/axios/axios/issues/1158
axiosClient.interceptors.request.use(config => ({ ...config, method: config.method && config.method.toUpperCase() as any }));
/**
* 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 (!httpRequest) {
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 any,
url: httpRequest.url,
headers: rawHeaders,
data: axiosBody,
transformResponse: undefined,
validateStatus: () => true,
// Workaround for https://github.com/axios/axios/issues/1362
maxContentLength: 1024 * 1024 * 1024 * 10,
responseType: httpRequest.streamResponseBody ? "stream" : "text",
cancelToken,
timeout: httpRequest.timeout
};
res = await axiosClient(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";
}