f308a0d109d636e3fd9cda4c9c6eb28a6d0acb27ca8a1e2e4330c52b9ded43013f9f9b8e7240392893c535c224dc809abfaa3afb8154d8a3a4faf4d311be81 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. import { map } from '../operators/map';
  2. import { Observable } from '../Observable';
  3. import { AjaxConfig, AjaxRequest, AjaxDirection, ProgressEventType } from './types';
  4. import { AjaxResponse } from './AjaxResponse';
  5. import { AjaxTimeoutError, AjaxError } from './errors';
  6. export interface AjaxCreationMethod {
  7. /**
  8. * Creates an observable that will perform an AJAX request using the
  9. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  10. * global scope by default.
  11. *
  12. * This is the most configurable option, and the basis for all other AJAX calls in the library.
  13. *
  14. * ## Example
  15. *
  16. * ```ts
  17. * import { ajax } from 'rxjs/ajax';
  18. * import { map, catchError, of } from 'rxjs';
  19. *
  20. * const obs$ = ajax({
  21. * method: 'GET',
  22. * url: 'https://api.github.com/users?per_page=5',
  23. * responseType: 'json'
  24. * }).pipe(
  25. * map(userResponse => console.log('users: ', userResponse)),
  26. * catchError(error => {
  27. * console.log('error: ', error);
  28. * return of(error);
  29. * })
  30. * );
  31. * ```
  32. */
  33. <T>(config: AjaxConfig): Observable<AjaxResponse<T>>;
  34. /**
  35. * Perform an HTTP GET using the
  36. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  37. * global scope. Defaults to a `responseType` of `"json"`.
  38. *
  39. * ## Example
  40. *
  41. * ```ts
  42. * import { ajax } from 'rxjs/ajax';
  43. * import { map, catchError, of } from 'rxjs';
  44. *
  45. * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe(
  46. * map(userResponse => console.log('users: ', userResponse)),
  47. * catchError(error => {
  48. * console.log('error: ', error);
  49. * return of(error);
  50. * })
  51. * );
  52. * ```
  53. */
  54. <T>(url: string): Observable<AjaxResponse<T>>;
  55. /**
  56. * Performs an HTTP GET using the
  57. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  58. * global scope by default, and a `responseType` of `"json"`.
  59. *
  60. * @param url The URL to get the resource from
  61. * @param headers Optional headers. Case-Insensitive.
  62. */
  63. get<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
  64. /**
  65. * Performs an HTTP POST using the
  66. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  67. * global scope by default, and a `responseType` of `"json"`.
  68. *
  69. * Before sending the value passed to the `body` argument, it is automatically serialized
  70. * based on the specified `responseType`. By default, a JavaScript object will be serialized
  71. * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
  72. * dictionary object to a url-encoded string.
  73. *
  74. * @param url The URL to get the resource from
  75. * @param body The content to send. The body is automatically serialized.
  76. * @param headers Optional headers. Case-Insensitive.
  77. */
  78. post<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
  79. /**
  80. * Performs an HTTP PUT using the
  81. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  82. * global scope by default, and a `responseType` of `"json"`.
  83. *
  84. * Before sending the value passed to the `body` argument, it is automatically serialized
  85. * based on the specified `responseType`. By default, a JavaScript object will be serialized
  86. * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
  87. * dictionary object to a url-encoded string.
  88. *
  89. * @param url The URL to get the resource from
  90. * @param body The content to send. The body is automatically serialized.
  91. * @param headers Optional headers. Case-Insensitive.
  92. */
  93. put<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
  94. /**
  95. * Performs an HTTP PATCH using the
  96. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  97. * global scope by default, and a `responseType` of `"json"`.
  98. *
  99. * Before sending the value passed to the `body` argument, it is automatically serialized
  100. * based on the specified `responseType`. By default, a JavaScript object will be serialized
  101. * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
  102. * dictionary object to a url-encoded string.
  103. *
  104. * @param url The URL to get the resource from
  105. * @param body The content to send. The body is automatically serialized.
  106. * @param headers Optional headers. Case-Insensitive.
  107. */
  108. patch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
  109. /**
  110. * Performs an HTTP DELETE using the
  111. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  112. * global scope by default, and a `responseType` of `"json"`.
  113. *
  114. * @param url The URL to get the resource from
  115. * @param headers Optional headers. Case-Insensitive.
  116. */
  117. delete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
  118. /**
  119. * Performs an HTTP GET using the
  120. * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
  121. * global scope by default, and returns the hydrated JavaScript object from the
  122. * response.
  123. *
  124. * @param url The URL to get the resource from
  125. * @param headers Optional headers. Case-Insensitive.
  126. */
  127. getJSON<T>(url: string, headers?: Record<string, string>): Observable<T>;
  128. }
  129. function ajaxGet<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
  130. return ajax({ method: 'GET', url, headers });
  131. }
  132. function ajaxPost<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
  133. return ajax({ method: 'POST', url, body, headers });
  134. }
  135. function ajaxDelete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
  136. return ajax({ method: 'DELETE', url, headers });
  137. }
  138. function ajaxPut<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
  139. return ajax({ method: 'PUT', url, body, headers });
  140. }
  141. function ajaxPatch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
  142. return ajax({ method: 'PATCH', url, body, headers });
  143. }
  144. const mapResponse = map((x: AjaxResponse<any>) => x.response);
  145. function ajaxGetJSON<T>(url: string, headers?: Record<string, string>): Observable<T> {
  146. return mapResponse(
  147. ajax<T>({
  148. method: 'GET',
  149. url,
  150. headers,
  151. })
  152. );
  153. }
  154. /**
  155. * There is an ajax operator on the Rx object.
  156. *
  157. * It creates an observable for an Ajax request with either a request object with
  158. * url, headers, etc or a string for a URL.
  159. *
  160. * ## Examples
  161. *
  162. * Using `ajax()` to fetch the response object that is being returned from API
  163. *
  164. * ```ts
  165. * import { ajax } from 'rxjs/ajax';
  166. * import { map, catchError, of } from 'rxjs';
  167. *
  168. * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe(
  169. * map(userResponse => console.log('users: ', userResponse)),
  170. * catchError(error => {
  171. * console.log('error: ', error);
  172. * return of(error);
  173. * })
  174. * );
  175. *
  176. * obs$.subscribe({
  177. * next: value => console.log(value),
  178. * error: err => console.log(err)
  179. * });
  180. * ```
  181. *
  182. * Using `ajax.getJSON()` to fetch data from API
  183. *
  184. * ```ts
  185. * import { ajax } from 'rxjs/ajax';
  186. * import { map, catchError, of } from 'rxjs';
  187. *
  188. * const obs$ = ajax.getJSON('https://api.github.com/users?per_page=5').pipe(
  189. * map(userResponse => console.log('users: ', userResponse)),
  190. * catchError(error => {
  191. * console.log('error: ', error);
  192. * return of(error);
  193. * })
  194. * );
  195. *
  196. * obs$.subscribe({
  197. * next: value => console.log(value),
  198. * error: err => console.log(err)
  199. * });
  200. * ```
  201. *
  202. * Using `ajax()` with object as argument and method POST with a two seconds delay
  203. *
  204. * ```ts
  205. * import { ajax } from 'rxjs/ajax';
  206. * import { map, catchError, of } from 'rxjs';
  207. *
  208. * const users = ajax({
  209. * url: 'https://httpbin.org/delay/2',
  210. * method: 'POST',
  211. * headers: {
  212. * 'Content-Type': 'application/json',
  213. * 'rxjs-custom-header': 'Rxjs'
  214. * },
  215. * body: {
  216. * rxjs: 'Hello World!'
  217. * }
  218. * }).pipe(
  219. * map(response => console.log('response: ', response)),
  220. * catchError(error => {
  221. * console.log('error: ', error);
  222. * return of(error);
  223. * })
  224. * );
  225. *
  226. * users.subscribe({
  227. * next: value => console.log(value),
  228. * error: err => console.log(err)
  229. * });
  230. * ```
  231. *
  232. * Using `ajax()` to fetch. An error object that is being returned from the request
  233. *
  234. * ```ts
  235. * import { ajax } from 'rxjs/ajax';
  236. * import { map, catchError, of } from 'rxjs';
  237. *
  238. * const obs$ = ajax('https://api.github.com/404').pipe(
  239. * map(userResponse => console.log('users: ', userResponse)),
  240. * catchError(error => {
  241. * console.log('error: ', error);
  242. * return of(error);
  243. * })
  244. * );
  245. *
  246. * obs$.subscribe({
  247. * next: value => console.log(value),
  248. * error: err => console.log(err)
  249. * });
  250. * ```
  251. */
  252. export const ajax: AjaxCreationMethod = (() => {
  253. const create = <T>(urlOrConfig: string | AjaxConfig) => {
  254. const config: AjaxConfig =
  255. typeof urlOrConfig === 'string'
  256. ? {
  257. url: urlOrConfig,
  258. }
  259. : urlOrConfig;
  260. return fromAjax<T>(config);
  261. };
  262. create.get = ajaxGet;
  263. create.post = ajaxPost;
  264. create.delete = ajaxDelete;
  265. create.put = ajaxPut;
  266. create.patch = ajaxPatch;
  267. create.getJSON = ajaxGetJSON;
  268. return create;
  269. })();
  270. const UPLOAD = 'upload';
  271. const DOWNLOAD = 'download';
  272. const LOADSTART = 'loadstart';
  273. const PROGRESS = 'progress';
  274. const LOAD = 'load';
  275. export function fromAjax<T>(init: AjaxConfig): Observable<AjaxResponse<T>> {
  276. return new Observable((destination) => {
  277. const config = {
  278. // Defaults
  279. async: true,
  280. crossDomain: false,
  281. withCredentials: false,
  282. method: 'GET',
  283. timeout: 0,
  284. responseType: 'json' as XMLHttpRequestResponseType,
  285. ...init,
  286. };
  287. const { queryParams, body: configuredBody, headers: configuredHeaders } = config;
  288. let url = config.url;
  289. if (!url) {
  290. throw new TypeError('url is required');
  291. }
  292. if (queryParams) {
  293. let searchParams: URLSearchParams;
  294. if (url.includes('?')) {
  295. // If the user has passed a URL with a querystring already in it,
  296. // we need to combine them. So we're going to split it. There
  297. // should only be one `?` in a valid URL.
  298. const parts = url.split('?');
  299. if (2 < parts.length) {
  300. throw new TypeError('invalid url');
  301. }
  302. // Add the passed queryParams to the params already in the url provided.
  303. searchParams = new URLSearchParams(parts[1]);
  304. // queryParams is converted to any because the runtime is *much* more permissive than
  305. // the types are.
  306. new URLSearchParams(queryParams as any).forEach((value, key) => searchParams.set(key, value));
  307. // We have to do string concatenation here, because `new URL(url)` does
  308. // not like relative URLs like `/this` without a base url, which we can't
  309. // specify, nor can we assume `location` will exist, because of node.
  310. url = parts[0] + '?' + searchParams;
  311. } else {
  312. // There is no preexisting querystring, so we can just use URLSearchParams
  313. // to convert the passed queryParams into the proper format and encodings.
  314. // queryParams is converted to any because the runtime is *much* more permissive than
  315. // the types are.
  316. searchParams = new URLSearchParams(queryParams as any);
  317. url = url + '?' + searchParams;
  318. }
  319. }
  320. // Normalize the headers. We're going to make them all lowercase, since
  321. // Headers are case insensitive by design. This makes it easier to verify
  322. // that we aren't setting or sending duplicates.
  323. const headers: Record<string, any> = {};
  324. if (configuredHeaders) {
  325. for (const key in configuredHeaders) {
  326. if (configuredHeaders.hasOwnProperty(key)) {
  327. headers[key.toLowerCase()] = configuredHeaders[key];
  328. }
  329. }
  330. }
  331. const crossDomain = config.crossDomain;
  332. // Set the x-requested-with header. This is a non-standard header that has
  333. // come to be a de facto standard for HTTP requests sent by libraries and frameworks
  334. // using XHR. However, we DO NOT want to set this if it is a CORS request. This is
  335. // because sometimes this header can cause issues with CORS. To be clear,
  336. // None of this is necessary, it's only being set because it's "the thing libraries do"
  337. // Starting back as far as JQuery, and continuing with other libraries such as Angular 1,
  338. // Axios, et al.
  339. if (!crossDomain && !('x-requested-with' in headers)) {
  340. headers['x-requested-with'] = 'XMLHttpRequest';
  341. }
  342. // Allow users to provide their XSRF cookie name and the name of a custom header to use to
  343. // send the cookie.
  344. const { withCredentials, xsrfCookieName, xsrfHeaderName } = config;
  345. if ((withCredentials || !crossDomain) && xsrfCookieName && xsrfHeaderName) {
  346. const xsrfCookie = document?.cookie.match(new RegExp(`(^|;\\s*)(${xsrfCookieName})=([^;]*)`))?.pop() ?? '';
  347. if (xsrfCookie) {
  348. headers[xsrfHeaderName] = xsrfCookie;
  349. }
  350. }
  351. // Examine the body and determine whether or not to serialize it
  352. // and set the content-type in `headers`, if we're able.
  353. const body = extractContentTypeAndMaybeSerializeBody(configuredBody, headers);
  354. // The final request settings.
  355. const _request: Readonly<AjaxRequest> = {
  356. ...config,
  357. // Set values we ensured above
  358. url,
  359. headers,
  360. body,
  361. };
  362. let xhr: XMLHttpRequest;
  363. // Create our XHR so we can get started.
  364. xhr = init.createXHR ? init.createXHR() : new XMLHttpRequest();
  365. {
  366. ///////////////////////////////////////////////////
  367. // set up the events before open XHR
  368. // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
  369. // You need to add the event listeners before calling open() on the request.
  370. // Otherwise the progress events will not fire.
  371. ///////////////////////////////////////////////////
  372. const { progressSubscriber, includeDownloadProgress = false, includeUploadProgress = false } = init;
  373. /**
  374. * Wires up an event handler that will emit an error when fired. Used
  375. * for timeout and abort events.
  376. * @param type The type of event we're treating as an error
  377. * @param errorFactory A function that creates the type of error to emit.
  378. */
  379. const addErrorEvent = (type: string, errorFactory: () => any) => {
  380. xhr.addEventListener(type, () => {
  381. const error = errorFactory();
  382. progressSubscriber?.error?.(error);
  383. destination.error(error);
  384. });
  385. };
  386. // If the request times out, handle errors appropriately.
  387. addErrorEvent('timeout', () => new AjaxTimeoutError(xhr, _request));
  388. // If the request aborts (due to a network disconnection or the like), handle
  389. // it as an error.
  390. addErrorEvent('abort', () => new AjaxError('aborted', xhr, _request));
  391. /**
  392. * Creates a response object to emit to the consumer.
  393. * @param direction the direction related to the event. Prefixes the event `type` in the
  394. * `AjaxResponse` object with "upload_" for events related to uploading and "download_"
  395. * for events related to downloading.
  396. * @param event the actual event object.
  397. */
  398. const createResponse = (direction: AjaxDirection, event: ProgressEvent) =>
  399. new AjaxResponse<T>(event, xhr, _request, `${direction}_${event.type as ProgressEventType}` as const);
  400. /**
  401. * Wires up an event handler that emits a Response object to the consumer, used for
  402. * all events that emit responses, loadstart, progress, and load.
  403. * Note that download load handling is a bit different below, because it has
  404. * more logic it needs to run.
  405. * @param target The target, either the XHR itself or the Upload object.
  406. * @param type The type of event to wire up
  407. * @param direction The "direction", used to prefix the response object that is
  408. * emitted to the consumer. (e.g. "upload_" or "download_")
  409. */
  410. const addProgressEvent = (target: any, type: string, direction: AjaxDirection) => {
  411. target.addEventListener(type, (event: ProgressEvent) => {
  412. destination.next(createResponse(direction, event));
  413. });
  414. };
  415. if (includeUploadProgress) {
  416. [LOADSTART, PROGRESS, LOAD].forEach((type) => addProgressEvent(xhr.upload, type, UPLOAD));
  417. }
  418. if (progressSubscriber) {
  419. [LOADSTART, PROGRESS].forEach((type) => xhr.upload.addEventListener(type, (e: any) => progressSubscriber?.next?.(e)));
  420. }
  421. if (includeDownloadProgress) {
  422. [LOADSTART, PROGRESS].forEach((type) => addProgressEvent(xhr, type, DOWNLOAD));
  423. }
  424. const emitError = (status?: number) => {
  425. const msg = 'ajax error' + (status ? ' ' + status : '');
  426. destination.error(new AjaxError(msg, xhr, _request));
  427. };
  428. xhr.addEventListener('error', (e) => {
  429. progressSubscriber?.error?.(e);
  430. emitError();
  431. });
  432. xhr.addEventListener(LOAD, (event) => {
  433. const { status } = xhr;
  434. // 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
  435. if (status < 400) {
  436. progressSubscriber?.complete?.();
  437. let response: AjaxResponse<T>;
  438. try {
  439. // This can throw in IE, because we end up needing to do a JSON.parse
  440. // of the response in some cases to produce object we'd expect from
  441. // modern browsers.
  442. response = createResponse(DOWNLOAD, event);
  443. } catch (err) {
  444. destination.error(err);
  445. return;
  446. }
  447. destination.next(response);
  448. destination.complete();
  449. } else {
  450. progressSubscriber?.error?.(event);
  451. emitError(status);
  452. }
  453. });
  454. }
  455. const { user, method, async } = _request;
  456. // open XHR
  457. if (user) {
  458. xhr.open(method, url, async, user, _request.password);
  459. } else {
  460. xhr.open(method, url, async);
  461. }
  462. // timeout, responseType and withCredentials can be set once the XHR is open
  463. if (async) {
  464. xhr.timeout = _request.timeout;
  465. xhr.responseType = _request.responseType;
  466. }
  467. if ('withCredentials' in xhr) {
  468. xhr.withCredentials = _request.withCredentials;
  469. }
  470. // set headers
  471. for (const key in headers) {
  472. if (headers.hasOwnProperty(key)) {
  473. xhr.setRequestHeader(key, headers[key]);
  474. }
  475. }
  476. // finally send the request
  477. if (body) {
  478. xhr.send(body);
  479. } else {
  480. xhr.send();
  481. }
  482. return () => {
  483. if (xhr && xhr.readyState !== 4 /*XHR done*/) {
  484. xhr.abort();
  485. }
  486. };
  487. });
  488. }
  489. /**
  490. * Examines the body to determine if we need to serialize it for them or not.
  491. * If the body is a type that XHR handles natively, we just allow it through,
  492. * otherwise, if the body is something that *we* can serialize for the user,
  493. * we will serialize it, and attempt to set the `content-type` header, if it's
  494. * not already set.
  495. * @param body The body passed in by the user
  496. * @param headers The normalized headers
  497. */
  498. function extractContentTypeAndMaybeSerializeBody(body: any, headers: Record<string, string>) {
  499. if (
  500. !body ||
  501. typeof body === 'string' ||
  502. isFormData(body) ||
  503. isURLSearchParams(body) ||
  504. isArrayBuffer(body) ||
  505. isFile(body) ||
  506. isBlob(body) ||
  507. isReadableStream(body)
  508. ) {
  509. // The XHR instance itself can handle serializing these, and set the content-type for us
  510. // so we don't need to do that. https://xhr.spec.whatwg.org/#the-send()-method
  511. return body;
  512. }
  513. if (isArrayBufferView(body)) {
  514. // This is a typed array (e.g. Float32Array or Uint8Array), or a DataView.
  515. // XHR can handle this one too: https://fetch.spec.whatwg.org/#concept-bodyinit-extract
  516. return body.buffer;
  517. }
  518. if (typeof body === 'object') {
  519. // If we have made it here, this is an object, probably a POJO, and we'll try
  520. // to serialize it for them. If this doesn't work, it will throw, obviously, which
  521. // is okay. The workaround for users would be to manually set the body to their own
  522. // serialized string (accounting for circular references or whatever), then set
  523. // the content-type manually as well.
  524. headers['content-type'] = headers['content-type'] ?? 'application/json;charset=utf-8';
  525. return JSON.stringify(body);
  526. }
  527. // If we've gotten past everything above, this is something we don't quite know how to
  528. // handle. Throw an error. This will be caught and emitted from the observable.
  529. throw new TypeError('Unknown body type');
  530. }
  531. const _toString = Object.prototype.toString;
  532. function toStringCheck(obj: any, name: string): boolean {
  533. return _toString.call(obj) === `[object ${name}]`;
  534. }
  535. function isArrayBuffer(body: any): body is ArrayBuffer {
  536. return toStringCheck(body, 'ArrayBuffer');
  537. }
  538. function isFile(body: any): body is File {
  539. return toStringCheck(body, 'File');
  540. }
  541. function isBlob(body: any): body is Blob {
  542. return toStringCheck(body, 'Blob');
  543. }
  544. function isArrayBufferView(body: any): body is ArrayBufferView {
  545. return typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body);
  546. }
  547. function isFormData(body: any): body is FormData {
  548. return typeof FormData !== 'undefined' && body instanceof FormData;
  549. }
  550. function isURLSearchParams(body: any): body is URLSearchParams {
  551. return typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams;
  552. }
  553. function isReadableStream(body: any): body is ReadableStream {
  554. return typeof ReadableStream !== 'undefined' && body instanceof ReadableStream;
  555. }