UNPKG

5.52 kBPlain TextView Raw
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License. See License.txt in the project root for license information.
3
4import { HttpClient } from "./httpClient";
5import { HttpHeaders } from "./httpHeaders";
6import { WebResourceLike, TransferProgressEvent } from "./webResource";
7import { HttpOperationResponse } from "./httpOperationResponse";
8import { RestError } from "./restError";
9
10/**
11 * A HttpClient implementation that uses XMLHttpRequest to send HTTP requests.
12 */
13export class XhrHttpClient implements HttpClient {
14 public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
15 const xhr = new XMLHttpRequest();
16
17 if (request.agentSettings) {
18 throw new Error("HTTP agent settings not supported in browser environment");
19 }
20
21 if (request.proxySettings) {
22 throw new Error("HTTP proxy is not supported in browser environment");
23 }
24
25 const abortSignal = request.abortSignal;
26 if (abortSignal) {
27 const listener = () => {
28 xhr.abort();
29 };
30 abortSignal.addEventListener("abort", listener);
31 xhr.addEventListener("readystatechange", () => {
32 if (xhr.readyState === XMLHttpRequest.DONE) {
33 abortSignal.removeEventListener("abort", listener);
34 }
35 });
36 }
37
38 addProgressListener(xhr.upload, request.onUploadProgress);
39 addProgressListener(xhr, request.onDownloadProgress);
40
41 if (request.formData) {
42 const formData = request.formData;
43 const requestForm = new FormData();
44 const appendFormValue = (key: string, value: any) => {
45 if (value && value.hasOwnProperty("value") && value.hasOwnProperty("options")) {
46 requestForm.append(key, value.value, value.options);
47 } else {
48 requestForm.append(key, value);
49 }
50 };
51 for (const formKey of Object.keys(formData)) {
52 const formValue = formData[formKey];
53 if (Array.isArray(formValue)) {
54 for (let j = 0; j < formValue.length; j++) {
55 appendFormValue(formKey, formValue[j]);
56 }
57 } else {
58 appendFormValue(formKey, formValue);
59 }
60 }
61
62 request.body = requestForm;
63 request.formData = undefined;
64 const contentType = request.headers.get("Content-Type");
65 if (contentType && contentType.indexOf("multipart/form-data") !== -1) {
66 // browser will automatically apply a suitable content-type header
67 request.headers.remove("Content-Type");
68 }
69 }
70
71 xhr.open(request.method, request.url);
72 xhr.timeout = request.timeout;
73 xhr.withCredentials = request.withCredentials;
74 for (const header of request.headers.headersArray()) {
75 xhr.setRequestHeader(header.name, header.value);
76 }
77 xhr.responseType = request.streamResponseBody ? "blob" : "text";
78
79 // tslint:disable-next-line:no-null-keyword
80 xhr.send(request.body === undefined ? null : request.body);
81
82 if (request.streamResponseBody) {
83 return new Promise((resolve, reject) => {
84 xhr.addEventListener("readystatechange", () => {
85 // Resolve as soon as headers are loaded
86 if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
87 const blobBody = new Promise<Blob>((resolve, reject) => {
88 xhr.addEventListener("load", () => {
89 resolve(xhr.response);
90 });
91 rejectOnTerminalEvent(request, xhr, reject);
92 });
93 resolve({
94 request,
95 status: xhr.status,
96 headers: parseHeaders(xhr),
97 blobBody,
98 });
99 }
100 });
101 rejectOnTerminalEvent(request, xhr, reject);
102 });
103 } else {
104 return new Promise(function (resolve, reject) {
105 xhr.addEventListener("load", () =>
106 resolve({
107 request,
108 status: xhr.status,
109 headers: parseHeaders(xhr),
110 bodyAsText: xhr.responseText,
111 })
112 );
113 rejectOnTerminalEvent(request, xhr, reject);
114 });
115 }
116 }
117}
118
119function addProgressListener(
120 xhr: XMLHttpRequestEventTarget,
121 listener?: (progress: TransferProgressEvent) => void
122) {
123 if (listener) {
124 xhr.addEventListener("progress", (rawEvent) =>
125 listener({
126 loadedBytes: rawEvent.loaded,
127 })
128 );
129 }
130}
131
132// exported locally for testing
133export function parseHeaders(xhr: XMLHttpRequest) {
134 const responseHeaders = new HttpHeaders();
135 const headerLines = xhr
136 .getAllResponseHeaders()
137 .trim()
138 .split(/[\r\n]+/);
139 for (const line of headerLines) {
140 const index = line.indexOf(":");
141 const headerName = line.slice(0, index);
142 const headerValue = line.slice(index + 2);
143 responseHeaders.set(headerName, headerValue);
144 }
145 return responseHeaders;
146}
147
148function rejectOnTerminalEvent(
149 request: WebResourceLike,
150 xhr: XMLHttpRequest,
151 reject: (err: any) => void
152) {
153 xhr.addEventListener("error", () =>
154 reject(
155 new RestError(
156 `Failed to send request to ${request.url}`,
157 RestError.REQUEST_SEND_ERROR,
158 undefined,
159 request
160 )
161 )
162 );
163 xhr.addEventListener("abort", () =>
164 reject(
165 new RestError("The request was aborted", RestError.REQUEST_ABORTED_ERROR, undefined, request)
166 )
167 );
168 xhr.addEventListener("timeout", () =>
169 reject(
170 new RestError(
171 `timeout of ${xhr.timeout}ms exceeded`,
172 RestError.REQUEST_SEND_ERROR,
173 undefined,
174 request
175 )
176 )
177 );
178}
179
\No newline at end of file