2018-06-23 00:04:19 +03:00
|
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
|
|
// Licensed under the MIT License. See License.txt in the project root for license information.
|
|
|
|
|
|
|
|
import { HttpClient } from "./httpClient";
|
|
|
|
import { HttpHeaders } from "./httpHeaders";
|
2018-06-23 02:47:39 +03:00
|
|
|
import { WebResource, TransferProgressEvent } from "./webResource";
|
|
|
|
import { HttpOperationResponse } from "./httpOperationResponse";
|
|
|
|
import { RestError } from "./restError";
|
2018-06-23 00:04:19 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A HttpClient implementation that uses XMLHttpRequest to send HTTP requests.
|
|
|
|
*/
|
|
|
|
export class XhrHttpClient implements HttpClient {
|
|
|
|
public sendRequest(request: WebResource): Promise<HttpOperationResponse> {
|
|
|
|
const xhr = new XMLHttpRequest();
|
2018-06-23 00:38:37 +03:00
|
|
|
|
2018-06-23 01:43:33 +03:00
|
|
|
const abortSignal = request.abortSignal;
|
2018-06-23 00:38:37 +03:00
|
|
|
if (abortSignal) {
|
|
|
|
const listener = () => {
|
|
|
|
xhr.abort();
|
|
|
|
};
|
|
|
|
abortSignal.addEventListener("abort", listener);
|
2018-06-23 01:23:08 +03:00
|
|
|
xhr.addEventListener("readystatechange", () => {
|
|
|
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
|
|
abortSignal.removeEventListener("abort", listener);
|
|
|
|
}
|
|
|
|
});
|
2018-06-23 00:38:37 +03:00
|
|
|
}
|
|
|
|
|
2018-06-23 01:43:33 +03:00
|
|
|
addProgressListener(xhr.upload, request.onUploadProgress);
|
|
|
|
addProgressListener(xhr, request.onDownloadProgress);
|
2018-06-23 00:38:37 +03:00
|
|
|
|
2018-06-23 01:23:08 +03:00
|
|
|
if (request.formData) {
|
|
|
|
const formData = request.formData;
|
|
|
|
const requestForm = new FormData();
|
|
|
|
const appendFormValue = (key: string, value: any) => {
|
|
|
|
if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
|
|
|
|
requestForm.append(key, value.value, value.options);
|
|
|
|
} else {
|
|
|
|
requestForm.append(key, value);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
for (const formKey of Object.keys(formData)) {
|
|
|
|
const formValue = formData[formKey];
|
|
|
|
if (Array.isArray(formValue)) {
|
|
|
|
for (let j = 0; j < formValue.length; j++) {
|
|
|
|
appendFormValue(formKey, formValue[j]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
appendFormValue(formKey, formValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
request.body = requestForm;
|
|
|
|
request.formData = undefined;
|
|
|
|
const contentType = request.headers.get("Content-Type");
|
|
|
|
if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
|
|
|
|
// browser will automatically apply a suitable content-type header
|
|
|
|
request.headers.remove("Content-Type");
|
|
|
|
}
|
2018-06-23 00:04:19 +03:00
|
|
|
}
|
|
|
|
|
2018-07-17 23:35:47 +03:00
|
|
|
xhr.timeout = request.timeout;
|
2018-07-06 14:16:28 +03:00
|
|
|
xhr.withCredentials = request.withCredentials;
|
2018-06-23 00:38:37 +03:00
|
|
|
xhr.open(request.method, request.url);
|
2018-06-23 01:23:08 +03:00
|
|
|
for (const header of request.headers.headersArray()) {
|
|
|
|
xhr.setRequestHeader(header.name, header.value);
|
|
|
|
}
|
2018-08-24 23:20:24 +03:00
|
|
|
xhr.responseType = request.streamResponseBody ? "blob" : "text";
|
2018-06-23 00:38:37 +03:00
|
|
|
xhr.send(request.body);
|
|
|
|
|
2018-08-24 23:20:24 +03:00
|
|
|
if (request.streamResponseBody) {
|
2018-06-23 00:04:19 +03:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
xhr.addEventListener("readystatechange", () => {
|
|
|
|
// Resolve as soon as headers are loaded
|
|
|
|
if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
|
2018-08-29 20:16:55 +03:00
|
|
|
const blobBody = new Promise<Blob>((resolve, reject) => {
|
2018-06-23 02:42:58 +03:00
|
|
|
xhr.addEventListener("load", () => {
|
|
|
|
resolve(xhr.response);
|
|
|
|
});
|
|
|
|
rejectOnTerminalEvent(request, xhr, reject);
|
|
|
|
});
|
2018-06-23 00:04:19 +03:00
|
|
|
resolve({
|
|
|
|
request,
|
|
|
|
status: xhr.status,
|
|
|
|
headers: parseHeaders(xhr),
|
2018-08-29 20:16:55 +03:00
|
|
|
blobBody
|
2018-06-23 00:04:19 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2018-06-23 01:23:08 +03:00
|
|
|
rejectOnTerminalEvent(request, xhr, reject);
|
2018-06-23 00:38:37 +03:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
xhr.addEventListener("load", () => resolve({
|
2018-07-04 11:27:26 +03:00
|
|
|
request,
|
|
|
|
status: xhr.status,
|
|
|
|
headers: parseHeaders(xhr),
|
2018-06-23 00:38:37 +03:00
|
|
|
bodyAsText: xhr.responseText
|
2018-06-23 02:47:39 +03:00
|
|
|
}));
|
2018-06-23 01:23:08 +03:00
|
|
|
rejectOnTerminalEvent(request, xhr, reject);
|
2018-06-23 00:04:19 +03:00
|
|
|
});
|
|
|
|
}
|
2018-06-23 00:38:37 +03:00
|
|
|
}
|
|
|
|
}
|
2018-06-23 00:04:19 +03:00
|
|
|
|
2018-06-23 00:38:37 +03:00
|
|
|
function addProgressListener(xhr: XMLHttpRequestEventTarget, listener?: (progress: TransferProgressEvent) => void) {
|
|
|
|
if (listener) {
|
|
|
|
xhr.addEventListener("progress", rawEvent => listener({
|
2018-08-30 22:46:54 +03:00
|
|
|
loadedBytes: rawEvent.loaded
|
2018-06-23 00:38:37 +03:00
|
|
|
}));
|
2018-06-23 00:04:19 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-23 23:13:48 +03:00
|
|
|
// exported locally for testing
|
|
|
|
export function parseHeaders(xhr: XMLHttpRequest) {
|
2018-06-23 00:04:19 +03:00
|
|
|
const responseHeaders = new HttpHeaders();
|
|
|
|
const headerLines = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
|
|
|
|
for (const line of headerLines) {
|
2018-08-23 23:13:48 +03:00
|
|
|
const index = line.indexOf(":");
|
|
|
|
const headerName = line.slice(0, index);
|
|
|
|
const headerValue = line.slice(index + 2);
|
2018-06-23 00:04:19 +03:00
|
|
|
responseHeaders.set(headerName, headerValue);
|
|
|
|
}
|
|
|
|
return responseHeaders;
|
|
|
|
}
|
2018-06-23 00:38:37 +03:00
|
|
|
|
2018-06-23 01:23:08 +03:00
|
|
|
function rejectOnTerminalEvent(request: WebResource, xhr: XMLHttpRequest, reject: (err: any) => void) {
|
2018-08-29 20:16:55 +03:00
|
|
|
xhr.addEventListener("error", () => reject(new RestError(`Failed to send request to ${request.url}`, RestError.REQUEST_SEND_ERROR, undefined, request)));
|
|
|
|
xhr.addEventListener("abort", () => reject(new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, request)));
|
|
|
|
xhr.addEventListener("timeout", () => reject(new RestError(`timeout of ${xhr.timeout}ms exceeded`, RestError.REQUEST_SEND_ERROR, undefined, request)));
|
2018-06-23 01:23:08 +03:00
|
|
|
}
|