export type ConfigureFn = (request: XMLHttpRequest) => any; export interface AjaxResponse { data: any; status: number; statusText: string; headers: string; request: XMLHttpRequest; } export type AjaxRequestData = FormData | any; export interface AjaxError extends Error { error: true; code: string; request: XMLHttpRequest; response?: AjaxResponse; } /* inspired by axios */ class AjaxRequest { public createError( message: string, code: string | null, request: XMLHttpRequest, response?: AjaxResponse ): AjaxError { const error: AjaxError = new Error(message) as AjaxError; error.error = true; if (code) { error.code = code; } error.request = request; error.response = response; return error; } public settle(resolve: any, reject: any, response: AjaxResponse) { const validateStatus = (status: number) => { return status >= 200 && status < 300; }; // Note: status is not exposed by XDomainRequest if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(this.createError('Request failed with status code ' + response.status, null, response.request, response)); } } public request( method: string, url: string, formData: AjaxRequestData | null = null, configureFn?: ConfigureFn ): Promise { return new Promise((resolve, reject) => { // tslint:disable-next-line let request = new XMLHttpRequest(); const loadEvent = 'onreadystatechange'; request.open(method, url, true); // Listen for ready state request[loadEvent] = () => { if (!request || request.readyState !== 4) { return; } // The request errored out and we didn't get a response, this will be // handled by onerror instead // With one exception: request that using file: protocol, most browsers // will return status as 0 even though it's a successful request if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { return; } // Prepare the response const responseHeaders = request.getAllResponseHeaders(); let responseData: any = request.responseText; const contentType = request.getResponseHeader('Content-Type'); if (contentType && contentType.indexOf('application/json') !== -1) { responseData = JSON.parse(responseData); } else { try { responseData = JSON.parse(responseData); } catch (e) { /* ignore, possibly non json response */ } } const response: AjaxResponse = { data: responseData, // IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201) status: request.status === 1223 ? 204 : request.status, statusText: request.status === 1223 ? 'No Content' : request.statusText, headers: responseHeaders, request, }; this.settle(resolve, reject, response); // Clean up request (request as any) = null; }; // Handle browser request cancellation (as opposed to a manual cancellation) request.onabort = () => { if (!request) { return; } reject(this.createError('Request aborted', 'ECONNABORTED', request)); // Clean up request (request as any) = null; }; // Handle low level network errors request.onerror = () => { // Real errors are hidden from us by the browser // onerror should only fire if it's a network error reject(this.createError('Network Error', null, request)); // Clean up request (request as any) = null; }; // Handle timeout request.ontimeout = () => { reject(this.createError('timeout exceeded', 'ECONNABORTED', request)); // Clean up request (request as any) = null; }; // // Handle progress if needed // if (typeof config.onDownloadProgress === 'function') { // request.addEventListener('progress', config.onDownloadProgress); // } // Not all browsers support upload events // if (typeof progressCallback === 'function' && request.upload) { // request.upload.addEventListener('progress', progressCallback); // } if (typeof configureFn === 'function') { configureFn(request); } request.send(formData); }); } public post(url: string, formData: AjaxRequestData, configureFn?: ConfigureFn) { return this.request('POST', url, formData, configureFn); } public delete(url: string, formData: AjaxRequestData, configureFn?: ConfigureFn) { return this.request('DELETE', url, formData, configureFn); } public put(url: string, formData: AjaxRequestData, configureFn?: ConfigureFn) { return this.request('PUT', url, formData, configureFn); } } export default new AjaxRequest();