axiosHttpClient.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from "axios";
  4. import { Transform, Readable } from "stream";
  5. import FormData from "form-data";
  6. import * as tough from "tough-cookie";
  7. import { HttpClient } from "./httpClient";
  8. import { HttpHeaders } from "./httpHeaders";
  9. import { HttpOperationResponse } from "./httpOperationResponse";
  10. import { RestError } from "./restError";
  11. import { WebResource, HttpRequestBody } from "./webResource";
  12. import * as tunnel from "tunnel";
  13. import { ProxySettings } from "./serviceClient";
  14. import * as http from "http";
  15. import * as https from "https";
  16. import { URLBuilder } from "./url";
  17. /**
  18. * A HttpClient implementation that uses axios to send HTTP requests.
  19. */
  20. export class AxiosHttpClient implements HttpClient {
  21. private readonly cookieJar = new tough.CookieJar();
  22. public async sendRequest(httpRequest: WebResource): Promise<HttpOperationResponse> {
  23. if (typeof httpRequest !== "object") {
  24. throw new Error("httpRequest (WebResource) cannot be null or undefined and must be of type object.");
  25. }
  26. if (httpRequest.formData) {
  27. const formData: any = httpRequest.formData;
  28. const requestForm = new FormData();
  29. const appendFormValue = (key: string, value: any) => {
  30. // value function probably returns a stream so we can provide a fresh stream on each retry
  31. if (typeof value === "function") {
  32. value = value();
  33. }
  34. if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
  35. requestForm.append(key, value.value, value.options);
  36. } else {
  37. requestForm.append(key, value);
  38. }
  39. };
  40. for (const formKey of Object.keys(formData)) {
  41. const formValue = formData[formKey];
  42. if (Array.isArray(formValue)) {
  43. for (let j = 0; j < formValue.length; j++) {
  44. appendFormValue(formKey, formValue[j]);
  45. }
  46. } else {
  47. appendFormValue(formKey, formValue);
  48. }
  49. }
  50. httpRequest.body = requestForm;
  51. httpRequest.formData = undefined;
  52. const contentType = httpRequest.headers.get("Content-Type");
  53. if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
  54. if (typeof requestForm.getBoundary === "function") {
  55. httpRequest.headers.set("Content-Type", `multipart/form-data; boundary=${requestForm.getBoundary()}`);
  56. } else {
  57. // browser will automatically apply a suitable content-type header
  58. httpRequest.headers.remove("Content-Type");
  59. }
  60. }
  61. }
  62. if (this.cookieJar && !httpRequest.headers.get("Cookie")) {
  63. const cookieString = await new Promise<string>((resolve, reject) => {
  64. this.cookieJar!.getCookieString(httpRequest.url, (err, cookie) => {
  65. if (err) {
  66. reject(err);
  67. } else {
  68. resolve(cookie);
  69. }
  70. });
  71. });
  72. httpRequest.headers.set("Cookie", cookieString);
  73. }
  74. const abortSignal = httpRequest.abortSignal;
  75. if (abortSignal && abortSignal.aborted) {
  76. throw new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, httpRequest);
  77. }
  78. let abortListener: (() => void) | undefined;
  79. const cancelToken = abortSignal && new axios.CancelToken(canceler => {
  80. abortListener = () => canceler();
  81. abortSignal.addEventListener("abort", abortListener);
  82. });
  83. const rawHeaders: { [headerName: string]: string } = httpRequest.headers.rawHeaders();
  84. const httpRequestBody: HttpRequestBody = httpRequest.body;
  85. let axiosBody =
  86. // Workaround for https://github.com/axios/axios/issues/755
  87. // tslint:disable-next-line:no-null-keyword
  88. typeof httpRequestBody === "undefined" ? null :
  89. typeof httpRequestBody === "function" ? httpRequestBody() :
  90. httpRequestBody;
  91. const onUploadProgress = httpRequest.onUploadProgress;
  92. if (onUploadProgress && axiosBody) {
  93. let loadedBytes = 0;
  94. const uploadReportStream = new Transform({
  95. transform: (chunk: string | Buffer, _encoding, callback) => {
  96. loadedBytes += chunk.length;
  97. onUploadProgress({ loadedBytes });
  98. callback(undefined, chunk);
  99. }
  100. });
  101. if (isReadableStream(axiosBody)) {
  102. axiosBody.pipe(uploadReportStream);
  103. } else {
  104. uploadReportStream.end(axiosBody);
  105. }
  106. axiosBody = uploadReportStream;
  107. }
  108. let res: AxiosResponse;
  109. try {
  110. const config: AxiosRequestConfig = {
  111. method: httpRequest.method as Method,
  112. url: httpRequest.url,
  113. headers: rawHeaders,
  114. data: axiosBody,
  115. transformResponse: (data) => { return data; },
  116. validateStatus: () => true,
  117. // Workaround for https://github.com/axios/axios/issues/1362
  118. maxContentLength: Infinity,
  119. responseType: httpRequest.streamResponseBody ? "stream" : "text",
  120. cancelToken,
  121. timeout: httpRequest.timeout,
  122. proxy: false
  123. };
  124. if (httpRequest.proxySettings) {
  125. const agent = createProxyAgent(httpRequest.url, httpRequest.proxySettings, httpRequest.headers);
  126. if (agent.isHttps) {
  127. config.httpsAgent = agent.agent;
  128. } else {
  129. config.httpAgent = agent.agent;
  130. }
  131. }
  132. // This hack is still required with 0.19.0 version of axios since axios tries to merge the
  133. // Content-Type header from it's config["<method name>"] where the method name is lower-case,
  134. // into the request header. It could be possible that the Content-Type header is not present
  135. // in the original request and this would create problems while creating the signature for
  136. // storage data plane sdks.
  137. axios.interceptors.request.use((config: AxiosRequestConfig) => ({
  138. ...config,
  139. method: (config.method as Method) && (config.method as Method).toUpperCase() as Method
  140. }));
  141. res = await axios.request(config);
  142. } catch (err) {
  143. if (err instanceof axios.Cancel) {
  144. throw new RestError(err.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
  145. } else {
  146. const axiosErr = err as AxiosError;
  147. throw new RestError(axiosErr.message, RestError.REQUEST_SEND_ERROR, undefined, httpRequest);
  148. }
  149. } finally {
  150. if (abortSignal && abortListener) {
  151. abortSignal.removeEventListener("abort", abortListener);
  152. }
  153. }
  154. const headers = new HttpHeaders(res.headers);
  155. const onDownloadProgress = httpRequest.onDownloadProgress;
  156. let responseBody: Readable | string = res.data;
  157. if (onDownloadProgress) {
  158. if (isReadableStream(responseBody)) {
  159. let loadedBytes = 0;
  160. const downloadReportStream = new Transform({
  161. transform: (chunk: string | Buffer, _encoding, callback) => {
  162. loadedBytes += chunk.length;
  163. onDownloadProgress({ loadedBytes });
  164. callback(undefined, chunk);
  165. }
  166. });
  167. responseBody.pipe(downloadReportStream);
  168. responseBody = downloadReportStream;
  169. } else {
  170. const length = parseInt(headers.get("Content-Length")!) || (responseBody as string).length || undefined;
  171. if (length) {
  172. // Calling callback for non-stream response for consistency with browser
  173. onDownloadProgress({ loadedBytes: length });
  174. }
  175. }
  176. }
  177. const operationResponse: HttpOperationResponse = {
  178. request: httpRequest,
  179. status: res.status,
  180. headers,
  181. readableStreamBody: httpRequest.streamResponseBody ? responseBody as Readable : undefined,
  182. bodyAsText: httpRequest.streamResponseBody ? undefined : responseBody as string
  183. };
  184. if (this.cookieJar) {
  185. const setCookieHeader = operationResponse.headers.get("Set-Cookie");
  186. if (setCookieHeader != undefined) {
  187. await new Promise((resolve, reject) => {
  188. this.cookieJar!.setCookie(setCookieHeader, httpRequest.url, (err) => {
  189. if (err) {
  190. reject(err);
  191. } else {
  192. resolve();
  193. }
  194. });
  195. });
  196. }
  197. }
  198. return operationResponse;
  199. }
  200. }
  201. function isReadableStream(body: any): body is Readable {
  202. return typeof body.pipe === "function";
  203. }
  204. declare type ProxyAgent = { isHttps: boolean; agent: http.Agent | https.Agent };
  205. export function createProxyAgent(requestUrl: string, proxySettings: ProxySettings, headers?: HttpHeaders): ProxyAgent {
  206. const tunnelOptions: tunnel.HttpsOverHttpsOptions = {
  207. proxy: {
  208. host: URLBuilder.parse(proxySettings.host).getHost(),
  209. port: proxySettings.port,
  210. headers: (headers && headers.rawHeaders()) || {}
  211. }
  212. };
  213. if ((proxySettings.username && proxySettings.password)) {
  214. tunnelOptions.proxy!.proxyAuth = `${proxySettings.username}:${proxySettings.password}`;
  215. }
  216. const requestScheme = URLBuilder.parse(requestUrl).getScheme() || "";
  217. const isRequestHttps = requestScheme.toLowerCase() === "https";
  218. const proxyScheme = URLBuilder.parse(proxySettings.host).getScheme() || "";
  219. const isProxyHttps = proxyScheme.toLowerCase() === "https";
  220. const proxyAgent = {
  221. isHttps: isRequestHttps,
  222. agent: createTunnel(isRequestHttps, isProxyHttps, tunnelOptions)
  223. };
  224. return proxyAgent;
  225. }
  226. export function createTunnel(isRequestHttps: boolean, isProxyHttps: boolean, tunnelOptions: tunnel.HttpsOverHttpsOptions): http.Agent | https.Agent {
  227. if (isRequestHttps && isProxyHttps) {
  228. return tunnel.httpsOverHttps(tunnelOptions);
  229. } else if (isRequestHttps && !isProxyHttps) {
  230. return tunnel.httpsOverHttp(tunnelOptions);
  231. } else if (!isRequestHttps && isProxyHttps) {
  232. return tunnel.httpOverHttps(tunnelOptions);
  233. } else {
  234. return tunnel.httpOverHttp(tunnelOptions);
  235. }
  236. }