webResource.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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 { HttpHeaders } from "./httpHeaders";
  4. import { OperationSpec } from "./operationSpec";
  5. import { Mapper, Serializer } from "./serializer";
  6. import { generateUuid } from "./util/utils";
  7. import { HttpOperationResponse } from "./httpOperationResponse";
  8. import { OperationResponse } from "./operationResponse";
  9. import { ProxySettings } from "./serviceClient";
  10. export type HttpMethods = "GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS" | "TRACE";
  11. export type HttpRequestBody = Blob | string | ArrayBuffer | ArrayBufferView | (() => NodeJS.ReadableStream);
  12. /**
  13. * Fired in response to upload or download progress.
  14. */
  15. export type TransferProgressEvent = {
  16. /**
  17. * The number of bytes loaded so far.
  18. */
  19. loadedBytes: number
  20. };
  21. /**
  22. * Allows the request to be aborted upon firing of the "abort" event.
  23. * Compatible with the browser built-in AbortSignal and common polyfills.
  24. */
  25. export interface AbortSignalLike {
  26. readonly aborted: boolean;
  27. addEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
  28. removeEventListener(type: "abort", listener: (this: AbortSignalLike, ev: any) => any, options?: any): void;
  29. }
  30. /**
  31. * Creates a new WebResource object.
  32. *
  33. * This class provides an abstraction over a REST call by being library / implementation agnostic and wrapping the necessary
  34. * properties to initiate a request.
  35. *
  36. * @constructor
  37. */
  38. export class WebResource {
  39. url: string;
  40. method: HttpMethods;
  41. body?: any;
  42. headers: HttpHeaders;
  43. /**
  44. * Whether or not the body of the HttpOperationResponse should be treated as a stream.
  45. */
  46. streamResponseBody?: boolean;
  47. /**
  48. * Whether or not the HttpOperationResponse should be deserialized. If this is undefined, then the
  49. * HttpOperationResponse should be deserialized.
  50. */
  51. shouldDeserialize?: boolean | ((response: HttpOperationResponse) => boolean);
  52. /**
  53. * A function that returns the proper OperationResponse for the given OperationSpec and
  54. * HttpOperationResponse combination. If this is undefined, then a simple status code lookup will
  55. * be used.
  56. */
  57. operationResponseGetter?: (operationSpec: OperationSpec, response: HttpOperationResponse) => (undefined | OperationResponse);
  58. formData?: any;
  59. query?: { [key: string]: any; };
  60. operationSpec?: OperationSpec;
  61. withCredentials: boolean;
  62. timeout: number;
  63. proxySettings?: ProxySettings;
  64. abortSignal?: AbortSignalLike;
  65. /** Callback which fires upon upload progress. */
  66. onUploadProgress?: (progress: TransferProgressEvent) => void;
  67. /** Callback which fires upon download progress. */
  68. onDownloadProgress?: (progress: TransferProgressEvent) => void;
  69. constructor(
  70. url?: string,
  71. method?: HttpMethods,
  72. body?: any,
  73. query?: { [key: string]: any; },
  74. headers?: { [key: string]: any; } | HttpHeaders,
  75. streamResponseBody?: boolean,
  76. withCredentials?: boolean,
  77. abortSignal?: AbortSignalLike,
  78. timeout?: number,
  79. onUploadProgress?: (progress: TransferProgressEvent) => void,
  80. onDownloadProgress?: (progress: TransferProgressEvent) => void,
  81. proxySettings?: ProxySettings) {
  82. this.streamResponseBody = streamResponseBody;
  83. this.url = url || "";
  84. this.method = method || "GET";
  85. this.headers = (headers instanceof HttpHeaders ? headers : new HttpHeaders(headers));
  86. this.body = body;
  87. this.query = query;
  88. this.formData = undefined;
  89. this.withCredentials = withCredentials || false;
  90. this.abortSignal = abortSignal;
  91. this.timeout = timeout || 0;
  92. this.onUploadProgress = onUploadProgress;
  93. this.onDownloadProgress = onDownloadProgress;
  94. this.proxySettings = proxySettings;
  95. }
  96. /**
  97. * Validates that the required properties such as method, url, headers["Content-Type"],
  98. * headers["accept-language"] are defined. It will throw an error if one of the above
  99. * mentioned properties are not defined.
  100. */
  101. validateRequestProperties(): void {
  102. if (!this.method) {
  103. throw new Error("WebResource.method is required.");
  104. }
  105. if (!this.url) {
  106. throw new Error("WebResource.url is required.");
  107. }
  108. }
  109. /**
  110. * Prepares the request.
  111. * @param {RequestPrepareOptions} options Options to provide for preparing the request.
  112. * @returns {WebResource} Returns the prepared WebResource (HTTP Request) object that needs to be given to the request pipeline.
  113. */
  114. prepare(options: RequestPrepareOptions): WebResource {
  115. if (!options) {
  116. throw new Error("options object is required");
  117. }
  118. if (options.method == undefined || typeof options.method.valueOf() !== "string") {
  119. throw new Error("options.method must be a string.");
  120. }
  121. if (options.url && options.pathTemplate) {
  122. throw new Error("options.url and options.pathTemplate are mutually exclusive. Please provide exactly one of them.");
  123. }
  124. if ((options.pathTemplate == undefined || typeof options.pathTemplate.valueOf() !== "string") && (options.url == undefined || typeof options.url.valueOf() !== "string")) {
  125. throw new Error("Please provide exactly one of options.pathTemplate or options.url.");
  126. }
  127. // set the url if it is provided.
  128. if (options.url) {
  129. if (typeof options.url !== "string") {
  130. throw new Error("options.url must be of type \"string\".");
  131. }
  132. this.url = options.url;
  133. }
  134. // set the method
  135. if (options.method) {
  136. const validMethods = ["GET", "PUT", "HEAD", "DELETE", "OPTIONS", "POST", "PATCH", "TRACE"];
  137. if (validMethods.indexOf(options.method.toUpperCase()) === -1) {
  138. throw new Error("The provided method \"" + options.method + "\" is invalid. Supported HTTP methods are: " + JSON.stringify(validMethods));
  139. }
  140. }
  141. this.method = (options.method.toUpperCase() as HttpMethods);
  142. // construct the url if path template is provided
  143. if (options.pathTemplate) {
  144. const { pathTemplate, pathParameters } = options;
  145. if (typeof pathTemplate !== "string") {
  146. throw new Error("options.pathTemplate must be of type \"string\".");
  147. }
  148. if (!options.baseUrl) {
  149. options.baseUrl = "https://management.azure.com";
  150. }
  151. const baseUrl = options.baseUrl;
  152. let url = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + (pathTemplate.startsWith("/") ? pathTemplate.slice(1) : pathTemplate);
  153. const segments = url.match(/({\w*\s*\w*})/ig);
  154. if (segments && segments.length) {
  155. if (!pathParameters) {
  156. throw new Error(`pathTemplate: ${pathTemplate} has been provided. Hence, options.pathParameters must also be provided.`);
  157. }
  158. segments.forEach(function (item) {
  159. const pathParamName = item.slice(1, -1);
  160. const pathParam = (pathParameters as { [key: string]: any })[pathParamName];
  161. if (pathParam === null || pathParam === undefined || !(typeof pathParam === "string" || typeof pathParam === "object")) {
  162. throw new Error(`pathTemplate: ${pathTemplate} contains the path parameter ${pathParamName}` +
  163. ` however, it is not present in ${pathParameters} - ${JSON.stringify(pathParameters, undefined, 2)}.` +
  164. `The value of the path parameter can either be a "string" of the form { ${pathParamName}: "some sample value" } or ` +
  165. `it can be an "object" of the form { "${pathParamName}": { value: "some sample value", skipUrlEncoding: true } }.`);
  166. }
  167. if (typeof pathParam.valueOf() === "string") {
  168. url = url.replace(item, encodeURIComponent(pathParam));
  169. }
  170. if (typeof pathParam.valueOf() === "object") {
  171. if (!pathParam.value) {
  172. throw new Error(`options.pathParameters[${pathParamName}] is of type "object" but it does not contain a "value" property.`);
  173. }
  174. if (pathParam.skipUrlEncoding) {
  175. url = url.replace(item, pathParam.value);
  176. } else {
  177. url = url.replace(item, encodeURIComponent(pathParam.value));
  178. }
  179. }
  180. });
  181. }
  182. this.url = url;
  183. }
  184. // append query parameters to the url if they are provided. They can be provided with pathTemplate or url option.
  185. if (options.queryParameters) {
  186. const queryParameters = options.queryParameters;
  187. if (typeof queryParameters !== "object") {
  188. throw new Error(`options.queryParameters must be of type object. It should be a JSON object ` +
  189. `of "query-parameter-name" as the key and the "query-parameter-value" as the value. ` +
  190. `The "query-parameter-value" may be fo type "string" or an "object" of the form { value: "query-parameter-value", skipUrlEncoding: true }.`);
  191. }
  192. // append question mark if it is not present in the url
  193. if (this.url && this.url.indexOf("?") === -1) {
  194. this.url += "?";
  195. }
  196. // construct queryString
  197. const queryParams = [];
  198. // We need to populate this.query as a dictionary if the request is being used for Sway's validateRequest().
  199. this.query = {};
  200. for (const queryParamName in queryParameters) {
  201. const queryParam: any = queryParameters[queryParamName];
  202. if (queryParam) {
  203. if (typeof queryParam === "string") {
  204. queryParams.push(queryParamName + "=" + encodeURIComponent(queryParam));
  205. this.query[queryParamName] = encodeURIComponent(queryParam);
  206. }
  207. else if (typeof queryParam === "object") {
  208. if (!queryParam.value) {
  209. throw new Error(`options.queryParameters[${queryParamName}] is of type "object" but it does not contain a "value" property.`);
  210. }
  211. if (queryParam.skipUrlEncoding) {
  212. queryParams.push(queryParamName + "=" + queryParam.value);
  213. this.query[queryParamName] = queryParam.value;
  214. } else {
  215. queryParams.push(queryParamName + "=" + encodeURIComponent(queryParam.value));
  216. this.query[queryParamName] = encodeURIComponent(queryParam.value);
  217. }
  218. }
  219. }
  220. }// end-of-for
  221. // append the queryString
  222. this.url += queryParams.join("&");
  223. }
  224. // add headers to the request if they are provided
  225. if (options.headers) {
  226. const headers = options.headers;
  227. for (const headerName of Object.keys(options.headers)) {
  228. this.headers.set(headerName, headers[headerName]);
  229. }
  230. }
  231. // ensure accept-language is set correctly
  232. if (!this.headers.get("accept-language")) {
  233. this.headers.set("accept-language", "en-US");
  234. }
  235. // ensure the request-id is set correctly
  236. if (!this.headers.get("x-ms-client-request-id") && !options.disableClientRequestId) {
  237. this.headers.set("x-ms-client-request-id", generateUuid());
  238. }
  239. // default
  240. if (!this.headers.get("Content-Type")) {
  241. this.headers.set("Content-Type", "application/json; charset=utf-8");
  242. }
  243. // set the request body. request.js automatically sets the Content-Length request header, so we need not set it explicilty
  244. this.body = options.body;
  245. if (options.body != undefined) {
  246. // body as a stream special case. set the body as-is and check for some special request headers specific to sending a stream.
  247. if (options.bodyIsStream) {
  248. if (!this.headers.get("Transfer-Encoding")) {
  249. this.headers.set("Transfer-Encoding", "chunked");
  250. }
  251. if (this.headers.get("Content-Type") !== "application/octet-stream") {
  252. this.headers.set("Content-Type", "application/octet-stream");
  253. }
  254. } else {
  255. if (options.serializationMapper) {
  256. this.body = new Serializer(options.mappers).serialize(options.serializationMapper, options.body, "requestBody");
  257. }
  258. if (!options.disableJsonStringifyOnBody) {
  259. this.body = JSON.stringify(options.body);
  260. }
  261. }
  262. }
  263. this.abortSignal = options.abortSignal;
  264. this.onDownloadProgress = options.onDownloadProgress;
  265. this.onUploadProgress = options.onUploadProgress;
  266. return this;
  267. }
  268. /**
  269. * Clone this WebResource HTTP request object.
  270. * @returns {WebResource} The clone of this WebResource HTTP request object.
  271. */
  272. clone(): WebResource {
  273. const result = new WebResource(
  274. this.url,
  275. this.method,
  276. this.body,
  277. this.query,
  278. this.headers && this.headers.clone(),
  279. this.streamResponseBody,
  280. this.withCredentials,
  281. this.abortSignal,
  282. this.timeout,
  283. this.onUploadProgress,
  284. this.onDownloadProgress);
  285. if (this.formData) {
  286. result.formData = this.formData;
  287. }
  288. if (this.operationSpec) {
  289. result.operationSpec = this.operationSpec;
  290. }
  291. if (this.shouldDeserialize) {
  292. result.shouldDeserialize = this.shouldDeserialize;
  293. }
  294. if (this.operationResponseGetter) {
  295. result.operationResponseGetter = this.operationResponseGetter;
  296. }
  297. return result;
  298. }
  299. }
  300. export interface RequestPrepareOptions {
  301. /**
  302. * The HTTP request method. Valid values are "GET", "PUT", "HEAD", "DELETE", "OPTIONS", "POST",
  303. * or "PATCH".
  304. */
  305. method: HttpMethods;
  306. /**
  307. * The request url. It may or may not have query parameters in it. Either provide the "url" or
  308. * provide the "pathTemplate" in the options object. Both the options are mutually exclusive.
  309. */
  310. url?: string;
  311. /**
  312. * A dictionary of query parameters to be appended to the url, where
  313. * the "key" is the "query-parameter-name" and the "value" is the "query-parameter-value".
  314. * The "query-parameter-value" can be of type "string" or it can be of type "object".
  315. * The "object" format should be used when you want to skip url encoding. While using the object format,
  316. * the object must have a property named value which provides the "query-parameter-value".
  317. * Example:
  318. * - query-parameter-value in "object" format: { "query-parameter-name": { value: "query-parameter-value", skipUrlEncoding: true } }
  319. * - query-parameter-value in "string" format: { "query-parameter-name": "query-parameter-value"}.
  320. * Note: "If options.url already has some query parameters, then the value provided in options.queryParameters will be appended to the url.
  321. */
  322. queryParameters?: { [key: string]: any | ParameterValue };
  323. /**
  324. * The path template of the request url. Either provide the "url" or provide the "pathTemplate" in
  325. * the options object. Both the options are mutually exclusive.
  326. * Example: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}"
  327. */
  328. pathTemplate?: string;
  329. /**
  330. * The base url of the request. Default value is: "https://management.azure.com". This is
  331. * applicable only with pathTemplate. If you are providing options.url then it is expected that
  332. * you provide the complete url.
  333. */
  334. baseUrl?: string;
  335. /**
  336. * A dictionary of path parameters that need to be replaced with actual values in the pathTemplate.
  337. * Here the key is the "path-parameter-name" and the value is the "path-parameter-value".
  338. * The "path-parameter-value" can be of type "string" or it can be of type "object".
  339. * The "object" format should be used when you want to skip url encoding. While using the object format,
  340. * the object must have a property named value which provides the "path-parameter-value".
  341. * Example:
  342. * - path-parameter-value in "object" format: { "path-parameter-name": { value: "path-parameter-value", skipUrlEncoding: true } }
  343. * - path-parameter-value in "string" format: { "path-parameter-name": "path-parameter-value" }.
  344. */
  345. pathParameters?: { [key: string]: any | ParameterValue };
  346. formData?: { [key: string]: any };
  347. /**
  348. * A dictionary of request headers that need to be applied to the request.
  349. * Here the key is the "header-name" and the value is the "header-value". The header-value MUST be of type string.
  350. * - ContentType must be provided with the key name as "Content-Type". Default value "application/json; charset=utf-8".
  351. * - "Transfer-Encoding" is set to "chunked" by default if "options.bodyIsStream" is set to true.
  352. * - "Content-Type" is set to "application/octet-stream" by default if "options.bodyIsStream" is set to true.
  353. * - "accept-language" by default is set to "en-US"
  354. * - "x-ms-client-request-id" by default is set to a new Guid. To not generate a guid for the request, please set options.disableClientRequestId to true
  355. */
  356. headers?: { [key: string]: any };
  357. /**
  358. * When set to true, instructs the client to not set "x-ms-client-request-id" header to a new Guid().
  359. */
  360. disableClientRequestId?: boolean;
  361. /**
  362. * The request body. It can be of any type. This value will be serialized if it is not a stream.
  363. */
  364. body?: any;
  365. /**
  366. * Provides information on how to serialize the request body.
  367. */
  368. serializationMapper?: Mapper;
  369. /**
  370. * A dictionary of mappers that may be used while [de]serialization.
  371. */
  372. mappers?: { [x: string]: any };
  373. /**
  374. * Provides information on how to deserialize the response body.
  375. */
  376. deserializationMapper?: object;
  377. /**
  378. * Indicates whether this method should JSON.stringify() the request body. Default value: false.
  379. */
  380. disableJsonStringifyOnBody?: boolean;
  381. /**
  382. * Indicates whether the request body is a stream (useful for file upload scenarios).
  383. */
  384. bodyIsStream?: boolean;
  385. abortSignal?: AbortSignalLike;
  386. onUploadProgress?: (progress: TransferProgressEvent) => void;
  387. onDownloadProgress?: (progress: TransferProgressEvent) => void;
  388. }
  389. /**
  390. * The Parameter value provided for path or query parameters in RequestPrepareOptions
  391. */
  392. export interface ParameterValue {
  393. value: any;
  394. skipUrlEncoding: boolean;
  395. [key: string]: any;
  396. }
  397. /**
  398. * Describes the base structure of the options object that will be used in every operation.
  399. */
  400. export interface RequestOptionsBase {
  401. /**
  402. * @property {object} [customHeaders] User defined custom request headers that
  403. * will be applied before the request is sent.
  404. */
  405. customHeaders?: { [key: string]: string };
  406. /**
  407. * The signal which can be used to abort requests.
  408. */
  409. abortSignal?: AbortSignalLike;
  410. /**
  411. * The number of milliseconds a request can take before automatically being terminated.
  412. */
  413. timeout?: number;
  414. /**
  415. * Callback which fires upon upload progress.
  416. */
  417. onUploadProgress?: (progress: TransferProgressEvent) => void;
  418. /**
  419. * Callback which fires upon download progress.
  420. */
  421. onDownloadProgress?: (progress: TransferProgressEvent) => void;
  422. [key: string]: any;
  423. }