1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 |
|
7 | var _StatusCode = require('./StatusCode');
|
8 |
|
9 | var _StatusCode2 = _interopRequireDefault(_StatusCode);
|
10 |
|
11 | var _GenericError = require('../error/GenericError');
|
12 |
|
13 | var _GenericError2 = _interopRequireDefault(_GenericError);
|
14 |
|
15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
16 |
|
17 | /**
|
18 | * An object representing the complete request parameters used to create and
|
19 | * send the HTTP request.
|
20 | * @typedef {Object} HttpProxy~RequestParams
|
21 | * @property {string} method The HTTP method.
|
22 | * @property {string} url The original URL to which to make the request.
|
23 | * @property {string} transformedUrl The actual URL to which to make the
|
24 | * request, created by applying the URL transformer to the
|
25 | * original URL.
|
26 | * @property {Object<string, (boolean|number|string|Date)>} data The request
|
27 | * data, sent as query or body.
|
28 | * @property {HttpAgent~RequestOptions} options The high-level request options
|
29 | * provided by the HTTP agent.
|
30 | */
|
31 |
|
32 | /**
|
33 | * An object that describes a failed HTTP request, providing
|
34 | * information about both the failure reported by the server and how the
|
35 | * request has been sent to the server.
|
36 | * @typedef {Object} HttpProxy~ErrorParams
|
37 | * @property {string} errorName An error name.
|
38 | * @property {number} status The HTTP response status code send by the
|
39 | * server.
|
40 | * @property {object} body The body of HTTP error response (detailed
|
41 | * information).
|
42 | * @property {Error} cause The low-level cause error.
|
43 | * @property {HttpProxy~RequestParams} params An object representing the
|
44 | * complete request parameters used to create and send the HTTP
|
45 | * request.
|
46 | */
|
47 |
|
48 | /**
|
49 | * Middleware proxy between {@link HttpAgent} implementations and the
|
50 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API Fetch API},
|
51 | * providing a Promise-oriented API for sending requests.
|
52 | */
|
53 | class HttpProxy {
|
54 | /**
|
55 | * Initializes the HTTP proxy.
|
56 | *
|
57 | * @param {UrlTransformer} transformer A transformer of URLs to which
|
58 | * requests are made.
|
59 | * @param {Window} window Helper for manipulating the global object `window`
|
60 | * regardless of the client/server-side environment.
|
61 | */
|
62 | constructor(transformer, window) {
|
63 | /**
|
64 | * A transformer of URLs to which requests are made.
|
65 | *
|
66 | * @type {UrlTransformer}
|
67 | */
|
68 | this._transformer = transformer;
|
69 |
|
70 | /**
|
71 | * Helper for manipulating the global object `window` regardless of the
|
72 | * client/server-side environment.
|
73 | *
|
74 | * @type {Window}
|
75 | */
|
76 | this._window = window;
|
77 |
|
78 | /**
|
79 | * The default HTTP headers to include with the HTTP requests, unless
|
80 | * overridden.
|
81 | *
|
82 | * @type {Map<string, string>}
|
83 | */
|
84 | this._defaultHeaders = new Map();
|
85 | }
|
86 |
|
87 | /**
|
88 | * Executes a HTTP request to the specified URL using the specified HTTP
|
89 | * method, carrying the provided data.
|
90 | *
|
91 | * @param {string} method The HTTP method to use.
|
92 | * @param {string} url The URL to which the request should be made.
|
93 | * @param {Object<string, (boolean|number|string|Date)>} data The data to
|
94 | * be send to the server. The data will be included as query
|
95 | * parameters if the request method is `GET` or `HEAD`, and as
|
96 | * a request body for any other request method.
|
97 | * @param {HttpAgent~RequestOptions=} options Optional request options.
|
98 | * @return {Promise.<HttpAgent~Response>} A promise that resolves to the server
|
99 | * response.
|
100 | */
|
101 | request(method, url, data, options) {
|
102 | const requestParams = this._composeRequestParams(method, url, data, options);
|
103 |
|
104 | return new Promise((resolve, reject) => {
|
105 | let requestTimeoutId;
|
106 |
|
107 | if (options.timeout) {
|
108 | requestTimeoutId = setTimeout(() => {
|
109 | reject(new _GenericError2.default('The HTTP request timed out', {
|
110 | status: _StatusCode2.default.TIMEOUT
|
111 | }));
|
112 | }, options.timeout);
|
113 | }
|
114 |
|
115 | const fetch = this._getFetchApi();
|
116 | fetch(this._composeRequestUrl(url, !this._shouldRequestHaveBody(method, data) ? data : {}), this._composeRequestInit(method, data, options)).then(response => {
|
117 | if (requestTimeoutId) {
|
118 | clearTimeout(requestTimeoutId);
|
119 | }
|
120 |
|
121 | const contentType = response.headers.get('content-type');
|
122 |
|
123 | if (response.status === _StatusCode2.default.NO_CONTENT) {
|
124 | return Promise.resolve([response, null]);
|
125 | } else if (contentType && contentType.includes('application/json')) {
|
126 | return response.json().then(body => [response, body]);
|
127 | } else {
|
128 | return response.text().then(body => [response, body]);
|
129 | }
|
130 | }).then(([response, responseBody]) => this._processResponse(requestParams, response, responseBody)).then(resolve, reject);
|
131 | }).catch(fetchError => {
|
132 | throw this._processError(fetchError, requestParams);
|
133 | });
|
134 | }
|
135 |
|
136 | /**
|
137 | * Sets the specified default HTTP header. The header will be sent with all
|
138 | * subsequent HTTP requests unless reconfigured using this method,
|
139 | * overridden by request options, or cleared by
|
140 | * {@link HttpProxy#clearDefaultHeaders} method.
|
141 | *
|
142 | * @param {string} header A header name.
|
143 | * @param {string} value A header value.
|
144 | */
|
145 | setDefaultHeader(header, value) {
|
146 | this._defaultHeaders.set(header, value);
|
147 | }
|
148 |
|
149 | /**
|
150 | * Clears all defaults headers sent with all requests.
|
151 | */
|
152 | clearDefaultHeaders() {
|
153 | this._defaultHeaders.clear();
|
154 | }
|
155 |
|
156 | /**
|
157 | * Gets an object that describes a failed HTTP request, providing
|
158 | * information about both the failure reported by the server and how the
|
159 | * request has been sent to the server.
|
160 | *
|
161 | * @param {string} method The HTTP method used to make the request.
|
162 | * @param {string} url The URL to which the request has been made.
|
163 | * @param {Object<string, (boolean|number|string|Date)>} data The data sent
|
164 | * with the request.
|
165 | * @param {HttpAgent~RequestOptions} options Optional request options.
|
166 | * @param {number} status The HTTP response status code send by the server.
|
167 | * @param {object} body The body of HTTP error response (detailed
|
168 | * information).
|
169 | * @param {Error} cause The low-level cause error.
|
170 | * @return {HttpProxy~ErrorParams} An object containing both the details of
|
171 | * the error and the request that lead to it.
|
172 | */
|
173 | getErrorParams(method, url, data, options, status, body, cause) {
|
174 | let params = this._composeRequestParams(method, url, data, options);
|
175 |
|
176 | if (typeof body === 'undefined') {
|
177 | body = {};
|
178 | }
|
179 |
|
180 | let error = { status, body, cause };
|
181 |
|
182 | switch (status) {
|
183 | case _StatusCode2.default.TIMEOUT:
|
184 | error.errorName = 'Timeout';
|
185 | break;
|
186 | case _StatusCode2.default.BAD_REQUEST:
|
187 | error.errorName = 'Bad Request';
|
188 | break;
|
189 | case _StatusCode2.default.UNAUTHORIZED:
|
190 | error.errorName = 'Unauthorized';
|
191 | break;
|
192 | case _StatusCode2.default.FORBIDDEN:
|
193 | error.errorName = 'Forbidden';
|
194 | break;
|
195 | case _StatusCode2.default.NOT_FOUND:
|
196 | error.errorName = 'Not Found';
|
197 | break;
|
198 | case _StatusCode2.default.SERVER_ERROR:
|
199 | error.errorName = 'Internal Server Error';
|
200 | break;
|
201 | default:
|
202 | error.errorName = 'Unknown';
|
203 | break;
|
204 | }
|
205 |
|
206 | return Object.assign(error, params);
|
207 | }
|
208 |
|
209 | /**
|
210 | * Returns `true` if cookies have to be processed manually by setting
|
211 | * `Cookie` HTTP header on requests and parsing the `Set-Cookie` HTTP
|
212 | * response header.
|
213 | *
|
214 | * The result of this method depends on the current application
|
215 | * environment, the client-side usually handles cookie processing
|
216 | * automatically, leading this method returning `false`.
|
217 | *
|
218 | * At the client-side, the method tests whether the client has cookies
|
219 | * enabled (which results in cookies being automatically processed by the
|
220 | * browser), and returns `true` or `false` accordingly.
|
221 | *
|
222 | * @return {boolean} `true` if cookies are not processed automatically by
|
223 | * the environment and have to be handled manually by parsing
|
224 | * response headers and setting request headers, otherwise `false`.
|
225 | */
|
226 | haveToSetCookiesManually() {
|
227 | return !this._window.isCookieEnabled();
|
228 | }
|
229 |
|
230 | /**
|
231 | * Processes the response received from the server.
|
232 | *
|
233 | * @param {HttpProxy~RequestParams} requestParams The original request's
|
234 | * parameters.
|
235 | * @param {Response} response The Fetch API's `Response` object representing
|
236 | * the server's response.
|
237 | * @param {*} responseBody The server's response body.
|
238 | * @return {HttpAgent~Response} The server's response along with all related
|
239 | * metadata.
|
240 | */
|
241 | _processResponse(requestParams, response, responseBody) {
|
242 | if (response.ok) {
|
243 | return {
|
244 | status: response.status,
|
245 | body: responseBody,
|
246 | params: requestParams,
|
247 | headers: this._headersToPlainObject(response.headers),
|
248 | headersRaw: response.headers,
|
249 | cached: false
|
250 | };
|
251 | } else {
|
252 | throw new _GenericError2.default('The request failed', {
|
253 | status: response.status,
|
254 | body: responseBody
|
255 | });
|
256 | }
|
257 | }
|
258 |
|
259 | /**
|
260 | * Converts the provided Fetch API's `Headers` object to a plain object.
|
261 | *
|
262 | * @param {Headers} headers The headers to convert.
|
263 | * @return {Object.<string, string>} Converted headers.
|
264 | */
|
265 | _headersToPlainObject(headers) {
|
266 | let plainHeaders = {};
|
267 |
|
268 | if (headers.entries) {
|
269 | for (let [key, value] of headers.entries()) {
|
270 | plainHeaders[key] = value;
|
271 | }
|
272 | } else if (headers.forEach) {
|
273 | /**
|
274 | * Check for forEach() has to be here because in old Firefoxes (versions lower than 44) there is not
|
275 | * possibility to iterate through all the headers - according to docs
|
276 | * (https://developer.mozilla.org/en-US/docs/Web/API/Headers) where is "entries(), keys(), values(), and support
|
277 | * of for...of" is supported from Firefox version 44
|
278 | */
|
279 | if (headers.getAll) {
|
280 | /**
|
281 | * @todo This branch should be removed with node-fetch release
|
282 | * 2.0.0.
|
283 | */
|
284 | headers.forEach((_, headerName) => {
|
285 | plainHeaders[headerName] = headers.getAll(headerName).join(', ');
|
286 | });
|
287 | } else if (headers.get) {
|
288 | /**
|
289 | * In case that Headers.getAll() from previous branch doesn't exist because it is obsolete and deprecated - in
|
290 | * newer versions of the Fetch spec, Headers.getAll() has been deleted, and Headers.get() has been updated to
|
291 | * fetch all header values instead of only the first one - according to docs
|
292 | * (https://developer.mozilla.org/en-US/docs/Web/API/Headers/getAll)
|
293 | */
|
294 | headers.forEach((_, headerName) => {
|
295 | plainHeaders[headerName] = headers.get(headerName);
|
296 | });
|
297 | } else {
|
298 | /**
|
299 | * @todo If Microsoft Edge supported headers.entries(), we'd remove
|
300 | * this branch.
|
301 | */
|
302 | headers.forEach((headerValue, headerName) => {
|
303 | plainHeaders[headerName] = headerValue;
|
304 | });
|
305 | }
|
306 | }
|
307 |
|
308 | return plainHeaders;
|
309 | }
|
310 |
|
311 | /**
|
312 | * Processes the provided Fetch API or internal error and creates an error
|
313 | * to expose to the calling API.
|
314 | *
|
315 | * @param {Error} fetchError The internal error to process.
|
316 | * @param {HttpProxy~RequestParams} requestParams An object representing the
|
317 | * complete request parameters used to create and send the HTTP
|
318 | * request.
|
319 | * @return {GenericError} The error to provide to the calling API.
|
320 | */
|
321 | _processError(fetchError, requestParams) {
|
322 | const errorParams = fetchError instanceof _GenericError2.default ? fetchError.getParams() : {};
|
323 | return this._createError(fetchError, requestParams, errorParams.status || _StatusCode2.default.SERVER_ERROR, errorParams.body || null);
|
324 | }
|
325 |
|
326 | /**
|
327 | * Creates an error that represents a failed HTTP request.
|
328 | *
|
329 | * @param {Error} cause The error's message.
|
330 | * @param {HttpProxy~RequestParams} requestParams An object representing the
|
331 | * complete request parameters used to create and send the HTTP
|
332 | * request.
|
333 | * @param {number} status Server's response HTTP status code.
|
334 | * @param {*} responseBody The body of the server's response, if any.
|
335 | * @return {GenericError} The error representing a failed HTTP request.
|
336 | */
|
337 | _createError(cause, requestParams, status, responseBody = null) {
|
338 | return new _GenericError2.default(cause.message, this.getErrorParams(requestParams.method, requestParams.url, requestParams.data, requestParams.options, status, responseBody, cause));
|
339 | }
|
340 |
|
341 | /**
|
342 | * Returns {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch window.fetch}
|
343 | * compatible API to use, depending on the method being used at the server
|
344 | * (polyfill) or client (native/polyfill) side.
|
345 | *
|
346 | * @return {function((string|Request), RequestInit=): Promise.<Response>} An
|
347 | * implementation of the Fetch API to use.
|
348 | */
|
349 | _getFetchApi() {
|
350 | const fetch = 'node-fetch';
|
351 |
|
352 | return this._window.isClient() ? this._window.getWindow().fetch : require(fetch);
|
353 | }
|
354 |
|
355 | /**
|
356 | * Composes an object representing the HTTP request parameters from the
|
357 | * provided arguments.
|
358 | *
|
359 | * @param {string} method The HTTP method to use.
|
360 | * @param {string} url The URL to which the request should be sent.
|
361 | * @param {Object<string, (boolean|number|string|Date)>} data The data to
|
362 | * send with the request.
|
363 | * @param {HttpAgent~RequestOptions} options Optional request options.
|
364 | * @return {HttpProxy~RequestParams} An object
|
365 | * representing the complete request parameters used to create and
|
366 | * send the HTTP request.
|
367 | */
|
368 | _composeRequestParams(method, url, data, options) {
|
369 | return {
|
370 | method,
|
371 | url,
|
372 | transformedUrl: this._transformer.transform(url),
|
373 | data,
|
374 | options
|
375 | };
|
376 | }
|
377 |
|
378 | /**
|
379 | * Composes an init object, which can be used as a second argument of
|
380 | * `window.fetch` method.
|
381 | *
|
382 | * @param {string} method The HTTP method to use.
|
383 | * @param {Object.<string, (boolean|number|string|Date)>} data The data to
|
384 | * be send with a request.
|
385 | * @param {HttpAgent~RequestOptions} options Options provided by the HTTP
|
386 | * agent.
|
387 | * @return {RequestInit} A `RequestInit` object of the Fetch API.
|
388 | */
|
389 | _composeRequestInit(method, data, options) {
|
390 | options.headers['Content-Type'] = this._getContentType(method, data, options);
|
391 |
|
392 | for (let [headerName, headerValue] of this._defaultHeaders) {
|
393 | options.headers[headerName] = headerValue;
|
394 | }
|
395 |
|
396 | let requestInit = {
|
397 | method: method.toUpperCase(),
|
398 | headers: options.headers,
|
399 | credentials: options.withCredentials ? 'include' : 'same-origin',
|
400 | redirect: 'follow'
|
401 | };
|
402 |
|
403 | if (this._shouldRequestHaveBody(method, data)) {
|
404 | requestInit.body = this._transformRequestBody(data, options.headers);
|
405 | }
|
406 |
|
407 | Object.assign(requestInit, options.fetchOptions || {});
|
408 |
|
409 | return requestInit;
|
410 | }
|
411 |
|
412 | /**
|
413 | * Gets a `Content-Type` header value for defined method, data and options.
|
414 | *
|
415 | * @param {string} method The HTTP method to use.
|
416 | * @param {Object.<string, (boolean|number|string|Date)>} data The data to
|
417 | * be send with a request.
|
418 | * @param {HttpAgent~RequestOptions} options Options provided by the HTTP
|
419 | * agent.
|
420 | * @return {string} A `Content-Type` header value.
|
421 | */
|
422 | _getContentType(method, data, options) {
|
423 | if (options.headers['Content-Type']) {
|
424 | return options.headers['Content-Type'];
|
425 | }
|
426 |
|
427 | if (this._shouldRequestHaveBody(method, data)) {
|
428 | return 'application/json';
|
429 | }
|
430 |
|
431 | return '';
|
432 | }
|
433 |
|
434 | /**
|
435 | * Transforms the provided URL using the current URL transformer and adds
|
436 | * the provided data to the URL's query string.
|
437 | *
|
438 | * @param {string} url The URL to prepare for use with the fetch API.
|
439 | * @param {Object<string, (boolean|number|string|Date)>} data The data to be
|
440 | * attached to the query string.
|
441 | * @return {string} The transformed URL with the provided data attached to
|
442 | * its query string.
|
443 | */
|
444 | _composeRequestUrl(url, data) {
|
445 | const transformedUrl = this._transformer.transform(url);
|
446 | const queryString = this._convertObjectToQueryString(data || {});
|
447 | const delimeter = queryString ? transformedUrl.includes('?') ? '&' : '?' : '';
|
448 |
|
449 | return `${transformedUrl}${delimeter}${queryString}`;
|
450 | }
|
451 |
|
452 | /**
|
453 | * Checks if a request should have a body (`GET` and `HEAD` requests don't
|
454 | * have a body).
|
455 | *
|
456 | * @param {string} method The HTTP method.
|
457 | * @param {Object.<string, (boolean|number|string|Date)>} data The data to
|
458 | * be send with a request.
|
459 | * @return {boolean} `true` if a request has a body, otherwise `false`.
|
460 | */
|
461 | _shouldRequestHaveBody(method, data) {
|
462 | return ['get', 'head'].indexOf(method.toLowerCase()) === -1 && data;
|
463 | }
|
464 |
|
465 | /**
|
466 | * Formats request body according to request headers.
|
467 | *
|
468 | * @param {Object.<string, (boolean|number|string|Date)>} data The data to
|
469 | * be send with a request.
|
470 | * @param {Object.<string, string>} headers Headers object from options provided by the HTTP
|
471 | * agent.
|
472 | * @returns {string|Object|FormData}
|
473 | * @private
|
474 | */
|
475 | _transformRequestBody(data, headers) {
|
476 | switch (headers['Content-Type']) {
|
477 | case 'application/json':
|
478 | return JSON.stringify(data);
|
479 | case 'application/x-www-form-urlencoded':
|
480 | return this._convertObjectToQueryString(data);
|
481 | case 'multipart/form-data':
|
482 | return this._convertObjectToFormData(data);
|
483 | default:
|
484 | return data;
|
485 | }
|
486 | }
|
487 |
|
488 | /**
|
489 | * Returns query string representation of the data parameter.
|
490 | * (Returned string does not contain ? at the beginning)
|
491 | *
|
492 | * @param {Object.<string, (boolean|number|string|Date)>} object The object to be converted
|
493 | * @returns {string} Query string representation of the given object
|
494 | * @private
|
495 | */
|
496 | _convertObjectToQueryString(object) {
|
497 | return Object.keys(object).map(key => [key, object[key]].map(encodeURIComponent).join('=')).join('&');
|
498 | }
|
499 |
|
500 | /**
|
501 | * Converts given data to FormData object.
|
502 | * If FormData object is not supported by the browser the original object is returned.
|
503 | *
|
504 | * @param {Object.<string, (boolean|number|string|Date)>} object The object to be converted
|
505 | * @returns {Object|FormData}
|
506 | * @private
|
507 | */
|
508 | _convertObjectToFormData(object) {
|
509 | const window = this._window.getWindow();
|
510 |
|
511 | if (!window || !window.FormData) {
|
512 | return object;
|
513 | }
|
514 | const formDataObject = new FormData();
|
515 | Object.keys(object).forEach(key => formDataObject.append(key, object[key]));
|
516 |
|
517 | return formDataObject;
|
518 | }
|
519 | }
|
520 | exports.default = HttpProxy;
|
521 |
|
522 | typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/http/HttpProxy', [], function (_export, _context) {
|
523 | ;
|
524 | return {
|
525 | setters: [],
|
526 | execute: function () {
|
527 | _export('default', exports.default);
|
528 | }
|
529 | };
|
530 | });
|