123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- import { map } from '../operators/map';
- import { Observable } from '../Observable';
- import { AjaxConfig, AjaxRequest, AjaxDirection, ProgressEventType } from './types';
- import { AjaxResponse } from './AjaxResponse';
- import { AjaxTimeoutError, AjaxError } from './errors';
- export interface AjaxCreationMethod {
- /**
- * Creates an observable that will perform an AJAX request using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default.
- *
- * This is the most configurable option, and the basis for all other AJAX calls in the library.
- *
- * ## Example
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const obs$ = ajax({
- * method: 'GET',
- * url: 'https://api.github.com/users?per_page=5',
- * responseType: 'json'
- * }).pipe(
- * map(userResponse => console.log('users: ', userResponse)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- * ```
- */
- <T>(config: AjaxConfig): Observable<AjaxResponse<T>>;
- /**
- * Perform an HTTP GET using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope. Defaults to a `responseType` of `"json"`.
- *
- * ## Example
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe(
- * map(userResponse => console.log('users: ', userResponse)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- * ```
- */
- <T>(url: string): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP GET using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and a `responseType` of `"json"`.
- *
- * @param url The URL to get the resource from
- * @param headers Optional headers. Case-Insensitive.
- */
- get<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP POST using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and a `responseType` of `"json"`.
- *
- * Before sending the value passed to the `body` argument, it is automatically serialized
- * based on the specified `responseType`. By default, a JavaScript object will be serialized
- * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
- * dictionary object to a url-encoded string.
- *
- * @param url The URL to get the resource from
- * @param body The content to send. The body is automatically serialized.
- * @param headers Optional headers. Case-Insensitive.
- */
- post<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP PUT using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and a `responseType` of `"json"`.
- *
- * Before sending the value passed to the `body` argument, it is automatically serialized
- * based on the specified `responseType`. By default, a JavaScript object will be serialized
- * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
- * dictionary object to a url-encoded string.
- *
- * @param url The URL to get the resource from
- * @param body The content to send. The body is automatically serialized.
- * @param headers Optional headers. Case-Insensitive.
- */
- put<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP PATCH using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and a `responseType` of `"json"`.
- *
- * Before sending the value passed to the `body` argument, it is automatically serialized
- * based on the specified `responseType`. By default, a JavaScript object will be serialized
- * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided
- * dictionary object to a url-encoded string.
- *
- * @param url The URL to get the resource from
- * @param body The content to send. The body is automatically serialized.
- * @param headers Optional headers. Case-Insensitive.
- */
- patch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP DELETE using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and a `responseType` of `"json"`.
- *
- * @param url The URL to get the resource from
- * @param headers Optional headers. Case-Insensitive.
- */
- delete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>;
- /**
- * Performs an HTTP GET using the
- * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in
- * global scope by default, and returns the hydrated JavaScript object from the
- * response.
- *
- * @param url The URL to get the resource from
- * @param headers Optional headers. Case-Insensitive.
- */
- getJSON<T>(url: string, headers?: Record<string, string>): Observable<T>;
- }
- function ajaxGet<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
- return ajax({ method: 'GET', url, headers });
- }
- function ajaxPost<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
- return ajax({ method: 'POST', url, body, headers });
- }
- function ajaxDelete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
- return ajax({ method: 'DELETE', url, headers });
- }
- function ajaxPut<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
- return ajax({ method: 'PUT', url, body, headers });
- }
- function ajaxPatch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> {
- return ajax({ method: 'PATCH', url, body, headers });
- }
- const mapResponse = map((x: AjaxResponse<any>) => x.response);
- function ajaxGetJSON<T>(url: string, headers?: Record<string, string>): Observable<T> {
- return mapResponse(
- ajax<T>({
- method: 'GET',
- url,
- headers,
- })
- );
- }
- /**
- * There is an ajax operator on the Rx object.
- *
- * It creates an observable for an Ajax request with either a request object with
- * url, headers, etc or a string for a URL.
- *
- * ## Examples
- *
- * Using `ajax()` to fetch the response object that is being returned from API
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe(
- * map(userResponse => console.log('users: ', userResponse)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- *
- * obs$.subscribe({
- * next: value => console.log(value),
- * error: err => console.log(err)
- * });
- * ```
- *
- * Using `ajax.getJSON()` to fetch data from API
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const obs$ = ajax.getJSON('https://api.github.com/users?per_page=5').pipe(
- * map(userResponse => console.log('users: ', userResponse)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- *
- * obs$.subscribe({
- * next: value => console.log(value),
- * error: err => console.log(err)
- * });
- * ```
- *
- * Using `ajax()` with object as argument and method POST with a two seconds delay
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const users = ajax({
- * url: 'https://httpbin.org/delay/2',
- * method: 'POST',
- * headers: {
- * 'Content-Type': 'application/json',
- * 'rxjs-custom-header': 'Rxjs'
- * },
- * body: {
- * rxjs: 'Hello World!'
- * }
- * }).pipe(
- * map(response => console.log('response: ', response)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- *
- * users.subscribe({
- * next: value => console.log(value),
- * error: err => console.log(err)
- * });
- * ```
- *
- * Using `ajax()` to fetch. An error object that is being returned from the request
- *
- * ```ts
- * import { ajax } from 'rxjs/ajax';
- * import { map, catchError, of } from 'rxjs';
- *
- * const obs$ = ajax('https://api.github.com/404').pipe(
- * map(userResponse => console.log('users: ', userResponse)),
- * catchError(error => {
- * console.log('error: ', error);
- * return of(error);
- * })
- * );
- *
- * obs$.subscribe({
- * next: value => console.log(value),
- * error: err => console.log(err)
- * });
- * ```
- */
- export const ajax: AjaxCreationMethod = (() => {
- const create = <T>(urlOrConfig: string | AjaxConfig) => {
- const config: AjaxConfig =
- typeof urlOrConfig === 'string'
- ? {
- url: urlOrConfig,
- }
- : urlOrConfig;
- return fromAjax<T>(config);
- };
- create.get = ajaxGet;
- create.post = ajaxPost;
- create.delete = ajaxDelete;
- create.put = ajaxPut;
- create.patch = ajaxPatch;
- create.getJSON = ajaxGetJSON;
- return create;
- })();
- const UPLOAD = 'upload';
- const DOWNLOAD = 'download';
- const LOADSTART = 'loadstart';
- const PROGRESS = 'progress';
- const LOAD = 'load';
- export function fromAjax<T>(init: AjaxConfig): Observable<AjaxResponse<T>> {
- return new Observable((destination) => {
- const config = {
- // Defaults
- async: true,
- crossDomain: false,
- withCredentials: false,
- method: 'GET',
- timeout: 0,
- responseType: 'json' as XMLHttpRequestResponseType,
- ...init,
- };
- const { queryParams, body: configuredBody, headers: configuredHeaders } = config;
- let url = config.url;
- if (!url) {
- throw new TypeError('url is required');
- }
- if (queryParams) {
- let searchParams: URLSearchParams;
- if (url.includes('?')) {
- // If the user has passed a URL with a querystring already in it,
- // we need to combine them. So we're going to split it. There
- // should only be one `?` in a valid URL.
- const parts = url.split('?');
- if (2 < parts.length) {
- throw new TypeError('invalid url');
- }
- // Add the passed queryParams to the params already in the url provided.
- searchParams = new URLSearchParams(parts[1]);
- // queryParams is converted to any because the runtime is *much* more permissive than
- // the types are.
- new URLSearchParams(queryParams as any).forEach((value, key) => searchParams.set(key, value));
- // We have to do string concatenation here, because `new URL(url)` does
- // not like relative URLs like `/this` without a base url, which we can't
- // specify, nor can we assume `location` will exist, because of node.
- url = parts[0] + '?' + searchParams;
- } else {
- // There is no preexisting querystring, so we can just use URLSearchParams
- // to convert the passed queryParams into the proper format and encodings.
- // queryParams is converted to any because the runtime is *much* more permissive than
- // the types are.
- searchParams = new URLSearchParams(queryParams as any);
- url = url + '?' + searchParams;
- }
- }
- // Normalize the headers. We're going to make them all lowercase, since
- // Headers are case insensitive by design. This makes it easier to verify
- // that we aren't setting or sending duplicates.
- const headers: Record<string, any> = {};
- if (configuredHeaders) {
- for (const key in configuredHeaders) {
- if (configuredHeaders.hasOwnProperty(key)) {
- headers[key.toLowerCase()] = configuredHeaders[key];
- }
- }
- }
- const crossDomain = config.crossDomain;
- // Set the x-requested-with header. This is a non-standard header that has
- // come to be a de facto standard for HTTP requests sent by libraries and frameworks
- // using XHR. However, we DO NOT want to set this if it is a CORS request. This is
- // because sometimes this header can cause issues with CORS. To be clear,
- // None of this is necessary, it's only being set because it's "the thing libraries do"
- // Starting back as far as JQuery, and continuing with other libraries such as Angular 1,
- // Axios, et al.
- if (!crossDomain && !('x-requested-with' in headers)) {
- headers['x-requested-with'] = 'XMLHttpRequest';
- }
- // Allow users to provide their XSRF cookie name and the name of a custom header to use to
- // send the cookie.
- const { withCredentials, xsrfCookieName, xsrfHeaderName } = config;
- if ((withCredentials || !crossDomain) && xsrfCookieName && xsrfHeaderName) {
- const xsrfCookie = document?.cookie.match(new RegExp(`(^|;\\s*)(${xsrfCookieName})=([^;]*)`))?.pop() ?? '';
- if (xsrfCookie) {
- headers[xsrfHeaderName] = xsrfCookie;
- }
- }
- // Examine the body and determine whether or not to serialize it
- // and set the content-type in `headers`, if we're able.
- const body = extractContentTypeAndMaybeSerializeBody(configuredBody, headers);
- // The final request settings.
- const _request: Readonly<AjaxRequest> = {
- ...config,
- // Set values we ensured above
- url,
- headers,
- body,
- };
- let xhr: XMLHttpRequest;
- // Create our XHR so we can get started.
- xhr = init.createXHR ? init.createXHR() : new XMLHttpRequest();
- {
- ///////////////////////////////////////////////////
- // set up the events before open XHR
- // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
- // You need to add the event listeners before calling open() on the request.
- // Otherwise the progress events will not fire.
- ///////////////////////////////////////////////////
- const { progressSubscriber, includeDownloadProgress = false, includeUploadProgress = false } = init;
- /**
- * Wires up an event handler that will emit an error when fired. Used
- * for timeout and abort events.
- * @param type The type of event we're treating as an error
- * @param errorFactory A function that creates the type of error to emit.
- */
- const addErrorEvent = (type: string, errorFactory: () => any) => {
- xhr.addEventListener(type, () => {
- const error = errorFactory();
- progressSubscriber?.error?.(error);
- destination.error(error);
- });
- };
- // If the request times out, handle errors appropriately.
- addErrorEvent('timeout', () => new AjaxTimeoutError(xhr, _request));
- // If the request aborts (due to a network disconnection or the like), handle
- // it as an error.
- addErrorEvent('abort', () => new AjaxError('aborted', xhr, _request));
- /**
- * Creates a response object to emit to the consumer.
- * @param direction the direction related to the event. Prefixes the event `type` in the
- * `AjaxResponse` object with "upload_" for events related to uploading and "download_"
- * for events related to downloading.
- * @param event the actual event object.
- */
- const createResponse = (direction: AjaxDirection, event: ProgressEvent) =>
- new AjaxResponse<T>(event, xhr, _request, `${direction}_${event.type as ProgressEventType}` as const);
- /**
- * Wires up an event handler that emits a Response object to the consumer, used for
- * all events that emit responses, loadstart, progress, and load.
- * Note that download load handling is a bit different below, because it has
- * more logic it needs to run.
- * @param target The target, either the XHR itself or the Upload object.
- * @param type The type of event to wire up
- * @param direction The "direction", used to prefix the response object that is
- * emitted to the consumer. (e.g. "upload_" or "download_")
- */
- const addProgressEvent = (target: any, type: string, direction: AjaxDirection) => {
- target.addEventListener(type, (event: ProgressEvent) => {
- destination.next(createResponse(direction, event));
- });
- };
- if (includeUploadProgress) {
- [LOADSTART, PROGRESS, LOAD].forEach((type) => addProgressEvent(xhr.upload, type, UPLOAD));
- }
- if (progressSubscriber) {
- [LOADSTART, PROGRESS].forEach((type) => xhr.upload.addEventListener(type, (e: any) => progressSubscriber?.next?.(e)));
- }
- if (includeDownloadProgress) {
- [LOADSTART, PROGRESS].forEach((type) => addProgressEvent(xhr, type, DOWNLOAD));
- }
- const emitError = (status?: number) => {
- const msg = 'ajax error' + (status ? ' ' + status : '');
- destination.error(new AjaxError(msg, xhr, _request));
- };
- xhr.addEventListener('error', (e) => {
- progressSubscriber?.error?.(e);
- emitError();
- });
- xhr.addEventListener(LOAD, (event) => {
- const { status } = xhr;
- // 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
- if (status < 400) {
- progressSubscriber?.complete?.();
- let response: AjaxResponse<T>;
- try {
- // This can throw in IE, because we end up needing to do a JSON.parse
- // of the response in some cases to produce object we'd expect from
- // modern browsers.
- response = createResponse(DOWNLOAD, event);
- } catch (err) {
- destination.error(err);
- return;
- }
- destination.next(response);
- destination.complete();
- } else {
- progressSubscriber?.error?.(event);
- emitError(status);
- }
- });
- }
- const { user, method, async } = _request;
- // open XHR
- if (user) {
- xhr.open(method, url, async, user, _request.password);
- } else {
- xhr.open(method, url, async);
- }
- // timeout, responseType and withCredentials can be set once the XHR is open
- if (async) {
- xhr.timeout = _request.timeout;
- xhr.responseType = _request.responseType;
- }
- if ('withCredentials' in xhr) {
- xhr.withCredentials = _request.withCredentials;
- }
- // set headers
- for (const key in headers) {
- if (headers.hasOwnProperty(key)) {
- xhr.setRequestHeader(key, headers[key]);
- }
- }
- // finally send the request
- if (body) {
- xhr.send(body);
- } else {
- xhr.send();
- }
- return () => {
- if (xhr && xhr.readyState !== 4 /*XHR done*/) {
- xhr.abort();
- }
- };
- });
- }
- /**
- * Examines the body to determine if we need to serialize it for them or not.
- * If the body is a type that XHR handles natively, we just allow it through,
- * otherwise, if the body is something that *we* can serialize for the user,
- * we will serialize it, and attempt to set the `content-type` header, if it's
- * not already set.
- * @param body The body passed in by the user
- * @param headers The normalized headers
- */
- function extractContentTypeAndMaybeSerializeBody(body: any, headers: Record<string, string>) {
- if (
- !body ||
- typeof body === 'string' ||
- isFormData(body) ||
- isURLSearchParams(body) ||
- isArrayBuffer(body) ||
- isFile(body) ||
- isBlob(body) ||
- isReadableStream(body)
- ) {
- // The XHR instance itself can handle serializing these, and set the content-type for us
- // so we don't need to do that. https://xhr.spec.whatwg.org/#the-send()-method
- return body;
- }
- if (isArrayBufferView(body)) {
- // This is a typed array (e.g. Float32Array or Uint8Array), or a DataView.
- // XHR can handle this one too: https://fetch.spec.whatwg.org/#concept-bodyinit-extract
- return body.buffer;
- }
- if (typeof body === 'object') {
- // If we have made it here, this is an object, probably a POJO, and we'll try
- // to serialize it for them. If this doesn't work, it will throw, obviously, which
- // is okay. The workaround for users would be to manually set the body to their own
- // serialized string (accounting for circular references or whatever), then set
- // the content-type manually as well.
- headers['content-type'] = headers['content-type'] ?? 'application/json;charset=utf-8';
- return JSON.stringify(body);
- }
- // If we've gotten past everything above, this is something we don't quite know how to
- // handle. Throw an error. This will be caught and emitted from the observable.
- throw new TypeError('Unknown body type');
- }
- const _toString = Object.prototype.toString;
- function toStringCheck(obj: any, name: string): boolean {
- return _toString.call(obj) === `[object ${name}]`;
- }
- function isArrayBuffer(body: any): body is ArrayBuffer {
- return toStringCheck(body, 'ArrayBuffer');
- }
- function isFile(body: any): body is File {
- return toStringCheck(body, 'File');
- }
- function isBlob(body: any): body is Blob {
- return toStringCheck(body, 'Blob');
- }
- function isArrayBufferView(body: any): body is ArrayBufferView {
- return typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body);
- }
- function isFormData(body: any): body is FormData {
- return typeof FormData !== 'undefined' && body instanceof FormData;
- }
- function isURLSearchParams(body: any): body is URLSearchParams {
- return typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams;
- }
- function isReadableStream(body: any): body is ReadableStream {
- return typeof ReadableStream !== 'undefined' && body instanceof ReadableStream;
- }
|