UNPKG

19.1 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _StatusCode = require('./StatusCode');
8
9var _StatusCode2 = _interopRequireDefault(_StatusCode);
10
11var _GenericError = require('../error/GenericError');
12
13var _GenericError2 = _interopRequireDefault(_GenericError);
14
15function _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 */
53class 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}
520exports.default = HttpProxy;
521
522typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/http/HttpProxy', [], function (_export, _context) {
523 'use strict';
524 return {
525 setters: [],
526 execute: function () {
527 _export('default', exports.default);
528 }
529 };
530});