1 |
|
2 |
|
3 |
|
4 | import { HttpClient } from "./httpClient";
|
5 | import { HttpHeaders } from "./httpHeaders";
|
6 | import { WebResourceLike, TransferProgressEvent } from "./webResource";
|
7 | import { HttpOperationResponse } from "./httpOperationResponse";
|
8 | import { RestError } from "./restError";
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | export 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 |
|
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 |
|
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 |
|
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 |
|
119 | function 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
|
133 | export 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 |
|
148 | function 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 |