FetchHttpClient.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. import { AbortError, HttpError, TimeoutError } from "./Errors";
  4. import { HttpClient, HttpResponse } from "./HttpClient";
  5. import { LogLevel } from "./ILogger";
  6. import { Platform, getGlobalThis, isArrayBuffer } from "./Utils";
  7. export class FetchHttpClient extends HttpClient {
  8. constructor(logger) {
  9. super();
  10. this._logger = logger;
  11. if (typeof fetch === "undefined") {
  12. // In order to ignore the dynamic require in webpack builds we need to do this magic
  13. // @ts-ignore: TS doesn't know about these names
  14. const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
  15. // Cookies aren't automatically handled in Node so we need to add a CookieJar to preserve cookies across requests
  16. this._jar = new (requireFunc("tough-cookie")).CookieJar();
  17. this._fetchType = requireFunc("node-fetch");
  18. // node-fetch doesn't have a nice API for getting and setting cookies
  19. // fetch-cookie will wrap a fetch implementation with a default CookieJar or a provided one
  20. this._fetchType = requireFunc("fetch-cookie")(this._fetchType, this._jar);
  21. }
  22. else {
  23. this._fetchType = fetch.bind(getGlobalThis());
  24. }
  25. if (typeof AbortController === "undefined") {
  26. // In order to ignore the dynamic require in webpack builds we need to do this magic
  27. // @ts-ignore: TS doesn't know about these names
  28. const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
  29. // Node needs EventListener methods on AbortController which our custom polyfill doesn't provide
  30. this._abortControllerType = requireFunc("abort-controller");
  31. }
  32. else {
  33. this._abortControllerType = AbortController;
  34. }
  35. }
  36. /** @inheritDoc */
  37. async send(request) {
  38. // Check that abort was not signaled before calling send
  39. if (request.abortSignal && request.abortSignal.aborted) {
  40. throw new AbortError();
  41. }
  42. if (!request.method) {
  43. throw new Error("No method defined.");
  44. }
  45. if (!request.url) {
  46. throw new Error("No url defined.");
  47. }
  48. const abortController = new this._abortControllerType();
  49. let error;
  50. // Hook our abortSignal into the abort controller
  51. if (request.abortSignal) {
  52. request.abortSignal.onabort = () => {
  53. abortController.abort();
  54. error = new AbortError();
  55. };
  56. }
  57. // If a timeout has been passed in, setup a timeout to call abort
  58. // Type needs to be any to fit window.setTimeout and NodeJS.setTimeout
  59. let timeoutId = null;
  60. if (request.timeout) {
  61. const msTimeout = request.timeout;
  62. timeoutId = setTimeout(() => {
  63. abortController.abort();
  64. this._logger.log(LogLevel.Warning, `Timeout from HTTP request.`);
  65. error = new TimeoutError();
  66. }, msTimeout);
  67. }
  68. if (request.content === "") {
  69. request.content = undefined;
  70. }
  71. if (request.content) {
  72. // Explicitly setting the Content-Type header for React Native on Android platform.
  73. request.headers = request.headers || {};
  74. if (isArrayBuffer(request.content)) {
  75. request.headers["Content-Type"] = "application/octet-stream";
  76. }
  77. else {
  78. request.headers["Content-Type"] = "text/plain;charset=UTF-8";
  79. }
  80. }
  81. let response;
  82. try {
  83. response = await this._fetchType(request.url, {
  84. body: request.content,
  85. cache: "no-cache",
  86. credentials: request.withCredentials === true ? "include" : "same-origin",
  87. headers: {
  88. "X-Requested-With": "XMLHttpRequest",
  89. ...request.headers,
  90. },
  91. method: request.method,
  92. mode: "cors",
  93. redirect: "follow",
  94. signal: abortController.signal,
  95. });
  96. }
  97. catch (e) {
  98. if (error) {
  99. throw error;
  100. }
  101. this._logger.log(LogLevel.Warning, `Error from HTTP request. ${e}.`);
  102. throw e;
  103. }
  104. finally {
  105. if (timeoutId) {
  106. clearTimeout(timeoutId);
  107. }
  108. if (request.abortSignal) {
  109. request.abortSignal.onabort = null;
  110. }
  111. }
  112. if (!response.ok) {
  113. const errorMessage = await deserializeContent(response, "text");
  114. throw new HttpError(errorMessage || response.statusText, response.status);
  115. }
  116. const content = deserializeContent(response, request.responseType);
  117. const payload = await content;
  118. return new HttpResponse(response.status, response.statusText, payload);
  119. }
  120. getCookieString(url) {
  121. let cookies = "";
  122. if (Platform.isNode && this._jar) {
  123. // @ts-ignore: unused variable
  124. this._jar.getCookies(url, (e, c) => cookies = c.join("; "));
  125. }
  126. return cookies;
  127. }
  128. }
  129. function deserializeContent(response, responseType) {
  130. let content;
  131. switch (responseType) {
  132. case "arraybuffer":
  133. content = response.arrayBuffer();
  134. break;
  135. case "text":
  136. content = response.text();
  137. break;
  138. case "blob":
  139. case "document":
  140. case "json":
  141. throw new Error(`${responseType} is not supported.`);
  142. default:
  143. content = response.text();
  144. break;
  145. }
  146. return content;
  147. }
  148. //# sourceMappingURL=FetchHttpClient.js.map