///
import { Observable, Observer } from 'rxjs';
import { RequestConfigBrowser, ResponseType } from '../Request';
import {
HttpDownloadProgressEvent,
HttpEvent,
HttpEventType,
HttpHeaderResponse,
HttpResponse,
HttpUploadProgressEvent
} from '../Response';
import { isFormData, isStandardBrowserEnv, isString, XSRF_HEADER_NAME } from '../utils/base';
import { cookies } from '../utils/cookies';
import { createError } from '../utils/createError';
import { getObserverHandler } from '../utils/getObserverHandler';
import { parseHeaders } from '../utils/parseHeaders';
import { parseJsonResponse } from '../utils/parseJsonResponse';
import { buildURL, isURLSameOrigin } from '../utils/urls';
import { xhrBackend } from './xhrBackend';
/**
* Determine an appropriate URL for the response, by checking either
* XMLHttpRequest.responseURL or the X-Request-URL header.
*/
function getResponseUrl(xhr: any): string | null {
if ('responseURL' in xhr && xhr.responseURL) {
return xhr.responseURL;
}
if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
return xhr.getResponseHeader('X-Request-URL');
}
return null;
}
const xhrAdapter = (config: RequestConfigBrowser) =>
new Observable((observer: Observer>) => {
const { reportProgress, withCredentials } = config;
const { emitError, emitComplete } = getObserverHandler(observer);
// Start by setting up the XHR object with request method, URL, and withCredentials flag.
const xhr: XMLHttpRequest = xhrBackend();
// This is the return from the Observable function, which is the
// request cancellation handler.
const tearDown = () => {
/* tslint:disable:no-use-before-declare */
// On a cancellation, remove all registered event listeners.
xhr.removeEventListener('error', onError);
xhr.removeEventListener('load', onLoad);
xhr.removeEventListener('abort', onAbort);
if (reportProgress) {
xhr.removeEventListener('progress', onDownProgress);
if (requestData !== null && xhr.upload) {
xhr.upload.removeEventListener('progress', onUpProgress);
}
}
/* tslint:enable:no-use-before-declare */
// Finally, abort the in-flight request.
xhr.abort();
};
if (!config.url || !config.method) {
emitError(createError(`Invalid request configuration`));
return tearDown;
}
const requestData = config.data;
const requestHeaders = config.headers;
if (isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
// HTTP basic authentication
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password || '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
xhr.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer));
// Add withCredentials to request if needed
if (!!withCredentials) {
xhr.withCredentials = true;
}
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (isStandardBrowserEnv()) {
// Add xsrf header
const xsrfValue =
(config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName
? cookies.read(config.xsrfCookieName)
: undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName || XSRF_HEADER_NAME] = xsrfValue;
}
}
// Add headers to the request
if ('setRequestHeader' in xhr && !!requestHeaders) {
Object.keys(requestHeaders)
.map(key => [key, requestHeaders[key]])
.filter(
([key]) =>
// Ignore remove Content-Type if data is undefined
key.toLowerCase() !== 'content-type' || !!requestData
)
.forEach(([key, value]) => xhr.setRequestHeader(key, value));
// Add an Accept header if one isn't present already.
if (!requestHeaders['Accept']) {
xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
}
}
// Add responseType to request if needed
if (config.responseType) {
try {
const responseType = config.responseType.toLowerCase() as XMLHttpRequestResponseType;
// JSON responses need to be processed as text. This is because if the server
// returns an XSSI-prefixed JSON response, the browser will fail to parse it,
// xhr.response will be null, and xhr.responseText cannot be accessed to
// retrieve the prefixed JSON data in order to strip the prefix. Thus, all JSON
// is parsed by first requesting text and then applying JSON.parse.
xhr.responseType = (responseType !== ResponseType.Json ? responseType : ResponseType.Text) as any;
} catch (e) {
// Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
// But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
if (config.responseType !== 'json') {
throw e;
}
}
}
// If progress events are enabled, response headers will be delivered
// in two events - the HttpHeaderResponse event and the full HttpResponse
// event. However, since response headers don't change in between these
// two events, it doesn't make sense to parse them twice. So headerResponse
// caches the data extracted from the response whenever it's first parsed,
// to ensure parsing isn't duplicated.
let headerResponse: HttpHeaderResponse | null = null;
// partialFromXhr extracts the HttpHeaderResponse from the current XMLHttpRequest
// state, and memoizes it into headerResponse.
const partialFromXhr = (): HttpHeaderResponse => {
if (headerResponse !== null) {
return headerResponse;
}
const status: number = xhr.status;
const statusText = xhr.statusText || 'OK';
// Parse headers from XMLHttpRequest
const headers = 'getAllResponseHeaders' in xhr ? parseHeaders(xhr.getAllResponseHeaders()) : null;
// Read the response URL from the XMLHttpResponse instance and fall back on the
// request URL.
const url = getResponseUrl(xhr) || config.url;
// Construct the HttpHeaderResponse and memoize it.
headerResponse = { headers, status, statusText, url: url!, config, type: HttpEventType.ResponseHeader };
return headerResponse;
};
// Next, a few closures are defined for the various events which XMLHttpRequest can
// emit. This allows them to be unregistered as event listeners later.
// First up is the load event, which represents a response being fully available.
const onLoad = () => {
// Read response state from the memoized partial data.
let { headers, status, statusText, url } = partialFromXhr();
// The body will be read out if present.
let body: any | null = null;
if (status !== 204) {
// Use XMLHttpRequest.response if set, responseText otherwise.
body = typeof xhr.response === 'undefined' ? xhr.responseText : xhr.response;
}
// Normalize another potential bug (this one comes from CORS).
if (status === 0) {
status = !!body ? 200 : 0;
}
// Check whether the body needs to be parsed as JSON (in many cases the browser
// will have done that already).
const parsedBody =
config.responseType === 'json' && isString(body) ? parseJsonResponse(status, body) : { ok: true, body };
// Prepare the response
const responseData =
!config.responseType || config.responseType === ResponseType.Text ? xhr.responseText : xhr.response;
const response: HttpResponse = {
type: HttpEventType.Response,
data: parsedBody.body || responseData || null,
status: status || xhr.status,
statusText: statusText,
headers,
config,
request: xhr,
responseUrl: url
};
//This will raised as error regardless existence of `validateStatus`
if (!parsedBody.ok) {
emitError(createError('Response parse failed', config, response.statusText, xhr, response));
} else {
// The full body has been received and delivered, no further events
// are possible. This request is complete.
emitComplete(response);
}
};
// The onError callback is called when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc. These are actual errors, and are
// transmitted on the error channel.
const onError = () => {
// Request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (xhr.status === 0 && !!xhr.responseURL && xhr.responseURL.indexOf('file:') === 0) {
return;
}
emitError(createError(xhr.statusText || 'Network Error', config, null, xhr));
};
// Handle browser request cancellation (as opposed to a manual cancellation)
// Teardown will remove listener before request abort, so unsubscription cancellation will not trigger error
const onAbort = () => emitError(createError('Request aborted', config, 'ECONNABORTED', xhr));
// The sentHeaders flag tracks whether the HttpResponseHeaders event
// has been sent on the stream. This is necessary to track if progress
// is enabled since the event will be sent on only the first download
// progerss event.
let sentHeaders = false;
// The download progress event handler, which is only registered if
// progress events are enabled.
const onDownProgress = (event: ProgressEvent) => {
// Send the HttpResponseHeaders event if it hasn't been sent already.
if (!sentHeaders) {
observer.next(partialFromXhr());
sentHeaders = true;
}
// Start building the download progress event to deliver on the response
// event stream.
let progressEvent: HttpDownloadProgressEvent = {
type: HttpEventType.DownloadProgress,
loaded: event.loaded
};
// Set the total number of bytes in the event if it's available.
if (event.lengthComputable) {
progressEvent.total = event.total;
}
// If the request was for text content and a partial response is
// available on XMLHttpRequest, include it in the progress event
// to allow for streaming reads.
if (xhr.responseType === ResponseType.Text && !!xhr.responseText) {
progressEvent.partialText = xhr.responseText;
}
// Finally, fire the event.
observer.next(progressEvent);
};
// The upload progress event handler, which is only registered if
// progress events are enabled.
const onUpProgress = (event: ProgressEvent) => {
// Upload progress events are simpler. Begin building the progress
// event.
let progress: HttpUploadProgressEvent = {
type: HttpEventType.UploadProgress,
loaded: event.loaded
};
// If the total number of bytes being uploaded is available, include
// it.
if (event.lengthComputable) {
progress.total = event.total;
}
// Send the event.
observer.next(progress);
};
// By default, register for load and error events.
xhr.addEventListener('load', onLoad);
xhr.addEventListener('error', onError);
// Progress events are only enabled if requested.
if (config.reportProgress) {
// Download progress is always enabled if requested.
xhr.addEventListener('progress', onDownProgress);
// Upload progress depends on whether there is a body to upload.
if (!!requestData && xhr.upload) {
xhr.upload.addEventListener('progress', onUpProgress);
}
}
// Fire the request, and notify the event stream that it was fired.
xhr.send(requestData || null);
observer.next({ type: HttpEventType.Sent });
return tearDown;
});
export { xhrAdapter as adapter };