xhrHttpClient.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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 { HttpClient } from "./httpClient";
  4. import { HttpHeaders } from "./httpHeaders";
  5. import { WebResource, TransferProgressEvent } from "./webResource";
  6. import { HttpOperationResponse } from "./httpOperationResponse";
  7. import { RestError } from "./restError";
  8. /**
  9. * A HttpClient implementation that uses XMLHttpRequest to send HTTP requests.
  10. */
  11. export class XhrHttpClient implements HttpClient {
  12. public sendRequest(request: WebResource): Promise<HttpOperationResponse> {
  13. const xhr = new XMLHttpRequest();
  14. if (request.proxySettings) {
  15. throw new Error("HTTP proxy is not supported in browser environment");
  16. }
  17. const abortSignal = request.abortSignal;
  18. if (abortSignal) {
  19. const listener = () => {
  20. xhr.abort();
  21. };
  22. abortSignal.addEventListener("abort", listener);
  23. xhr.addEventListener("readystatechange", () => {
  24. if (xhr.readyState === XMLHttpRequest.DONE) {
  25. abortSignal.removeEventListener("abort", listener);
  26. }
  27. });
  28. }
  29. addProgressListener(xhr.upload, request.onUploadProgress);
  30. addProgressListener(xhr, request.onDownloadProgress);
  31. if (request.formData) {
  32. const formData = request.formData;
  33. const requestForm = new FormData();
  34. const appendFormValue = (key: string, value: any) => {
  35. if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
  36. requestForm.append(key, value.value, value.options);
  37. } else {
  38. requestForm.append(key, value);
  39. }
  40. };
  41. for (const formKey of Object.keys(formData)) {
  42. const formValue = formData[formKey];
  43. if (Array.isArray(formValue)) {
  44. for (let j = 0; j < formValue.length; j++) {
  45. appendFormValue(formKey, formValue[j]);
  46. }
  47. } else {
  48. appendFormValue(formKey, formValue);
  49. }
  50. }
  51. request.body = requestForm;
  52. request.formData = undefined;
  53. const contentType = request.headers.get("Content-Type");
  54. if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
  55. // browser will automatically apply a suitable content-type header
  56. request.headers.remove("Content-Type");
  57. }
  58. }
  59. xhr.open(request.method, request.url);
  60. xhr.timeout = request.timeout;
  61. xhr.withCredentials = request.withCredentials;
  62. for (const header of request.headers.headersArray()) {
  63. xhr.setRequestHeader(header.name, header.value);
  64. }
  65. xhr.responseType = request.streamResponseBody ? "blob" : "text";
  66. // tslint:disable-next-line:no-null-keyword
  67. xhr.send(request.body === undefined ? null : request.body);
  68. if (request.streamResponseBody) {
  69. return new Promise((resolve, reject) => {
  70. xhr.addEventListener("readystatechange", () => {
  71. // Resolve as soon as headers are loaded
  72. if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
  73. const blobBody = new Promise<Blob>((resolve, reject) => {
  74. xhr.addEventListener("load", () => {
  75. resolve(xhr.response);
  76. });
  77. rejectOnTerminalEvent(request, xhr, reject);
  78. });
  79. resolve({
  80. request,
  81. status: xhr.status,
  82. headers: parseHeaders(xhr),
  83. blobBody
  84. });
  85. }
  86. });
  87. rejectOnTerminalEvent(request, xhr, reject);
  88. });
  89. } else {
  90. return new Promise(function (resolve, reject) {
  91. xhr.addEventListener("load", () => resolve({
  92. request,
  93. status: xhr.status,
  94. headers: parseHeaders(xhr),
  95. bodyAsText: xhr.responseText
  96. }));
  97. rejectOnTerminalEvent(request, xhr, reject);
  98. });
  99. }
  100. }
  101. }
  102. function addProgressListener(xhr: XMLHttpRequestEventTarget, listener?: (progress: TransferProgressEvent) => void) {
  103. if (listener) {
  104. xhr.addEventListener("progress", rawEvent => listener({
  105. loadedBytes: rawEvent.loaded
  106. }));
  107. }
  108. }
  109. // exported locally for testing
  110. export function parseHeaders(xhr: XMLHttpRequest) {
  111. const responseHeaders = new HttpHeaders();
  112. const headerLines = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
  113. for (const line of headerLines) {
  114. const index = line.indexOf(":");
  115. const headerName = line.slice(0, index);
  116. const headerValue = line.slice(index + 2);
  117. responseHeaders.set(headerName, headerValue);
  118. }
  119. return responseHeaders;
  120. }
  121. function rejectOnTerminalEvent(request: WebResource, xhr: XMLHttpRequest, reject: (err: any) => void) {
  122. xhr.addEventListener("error", () => reject(new RestError(`Failed to send request to ${request.url}`, RestError.REQUEST_SEND_ERROR, undefined, request)));
  123. xhr.addEventListener("abort", () => reject(new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, request)));
  124. xhr.addEventListener("timeout", () => reject(new RestError(`timeout of ${xhr.timeout}ms exceeded`, RestError.REQUEST_SEND_ERROR, undefined, request)));
  125. }