UNPKG

metaapi.cloud-sdk

Version:

SDK for MetaApi, a professional cloud forex API which includes MetaTrader REST API and MetaTrader websocket API. Supports both MetaTrader 5 (MT5) and MetaTrader 4 (MT4). CopyFactory copy trading API included. (https://metaapi.cloud)

204 lines (179 loc) 6.42 kB
'use strict'; const axios = require('@axios'); import { UnauthorizedError, ForbiddenError, ApiError, ValidationError, InternalError, NotFoundError, TooManyRequestsError, ConflictError } from '../../clients/errorHandler'; import TimeoutError from '../../clients/timeoutError'; /** * HTTP client library based on request-promise */ export default class HttpClient { private _timeout: number; private _extendedTimeout: number; private _retries: any; private _minRetryDelay: number; private _maxRetryDelay: number; /** * @typedef {Object} RetryOptions retry options * @property {Number} [retries] the number of attempts to retry failed request, default 5 * @property {Number} [minDelayInSeconds] minimum delay in seconds before retrying, default 1 * @property {Number} [maxDelayInSeconds] maximum delay in seconds before retrying, default 30 */ /** * Constructs HttpClient class instance * @param {Number} [timeout] request timeout in seconds * @param {Number} [extendedTimeout] request timeout in seconds * @param {RetryOptions} [retryOpts] retry options */ constructor(timeout = 10, extendedTimeout = 70, retryOpts: RetryOptions = {}) { this._timeout = timeout * 1000; this._extendedTimeout = extendedTimeout * 1000; this._retries = retryOpts.retries || 5; this._minRetryDelay = (retryOpts.minDelayInSeconds || 1) * 1000; this._maxRetryDelay = (retryOpts.maxDelayInSeconds || 30) * 1000; } /** * Performs a request. Response errors are returned as ApiError or subclasses. * @param {Object} options request options * @param {Boolean} isExtendedTimeout whether to run the request with an extended timeout * @returns {Object|String|any} request result */ async request(options, isExtendedTimeout?) { options.timeout = isExtendedTimeout ? this._extendedTimeout : this._timeout; try { const response = await this._makeRequest(options); return (response && response.data) || undefined; } catch (err) { throw this._convertError(err); } } /** * Performs a request with a failover. Response errors are returned as ApiError or subclasses. * @param {Object} options request options * @returns {Object|String|any} request result */ async requestWithFailover(options, retryCounter = 0, endTime = Date.now() + this._maxRetryDelay * this._retries) { options.timeout = this._timeout; let retryAfterSeconds = 0; options.callback = (e, res) => { if (res && res.status === 202) { retryAfterSeconds = res.headers['retry-after'] ?? res.data?.metadata?.recommendedRetryTime; if (isNaN(retryAfterSeconds)) { retryAfterSeconds = Math.max((new Date(retryAfterSeconds).getTime() - Date.now()) / 1000, 1); } } }; let body; try { const response = await this._makeRequest(options); options.callback(null, response); body = (response && response.data) || undefined; } catch (err) { retryCounter = await this._handleError(err, retryCounter, endTime); return this.requestWithFailover(options, retryCounter, endTime); } if (retryAfterSeconds) { await this._handleRetry(endTime, retryAfterSeconds * 1000); body = await this.requestWithFailover(options, retryCounter, endTime); } return body; } _makeRequest(options) { return axios({ transitional: { clarifyTimeoutError: true }, ...options }); } async _wait(pause) { await new Promise(res => setTimeout(res, pause)); } async _handleRetry(endTime, retryAfter) { if (endTime > Date.now() + retryAfter) { await this._wait(retryAfter); } else { throw new TimeoutError('Timed out waiting for the response'); } } async _handleError(err, retryCounter, endTime) { const error = this._convertError(err); if (['ConflictError', 'InternalError', 'ApiError', 'TimeoutError'].includes(error.name) && retryCounter < this._retries) { const pause = Math.min(Math.pow(2, retryCounter) * this._minRetryDelay, this._maxRetryDelay); await this._wait(pause); return retryCounter + 1; } else if (error.name === 'TooManyRequestsError') { const retryTime = new Date((error as TooManyRequestsError).metadata.recommendedRetryTime); if (retryTime < endTime) { await this._wait(retryTime.getTime() - Date.now()); return retryCounter; } } throw error; } // eslint-disable-next-line complexity _convertError(err) { const errorResponse = err.response || {}; const errorData = errorResponse.data || {}; const status = errorResponse.status || err.status; const url = err?.config?.url; const errMsgDefault = errorData.message || err.code || err.message; const errMsg = errorData.message || err.message; switch (status) { case 400: return new ValidationError(errMsg, errorData.details || err.details, url); case 401: return new UnauthorizedError(errMsg, url); case 403: return new ForbiddenError(errMsg, url); case 404: return new NotFoundError(errMsg, url); case 409: return new ConflictError(errMsg, url); case 429: return new TooManyRequestsError(errMsg, errorData.metadata || err.metadata, url); case 500: return new InternalError(errMsg, url); default: return new ApiError(ApiError, errMsgDefault, status, url); } } } /** * HTTP client service mock for tests */ export class HttpClientMock extends HttpClient { _requestFn: any; /** * Constructs HTTP client mock * @param {Function(options:Object):Promise} requestFn mocked request function * @param {Number} timeout request timeout in seconds * @param {RetryOptions} retryOpts retry options */ constructor(requestFn, timeout?, extendedTimeout?, retryOpts?) { super(timeout, extendedTimeout, retryOpts); this._requestFn = requestFn; } _makeRequest(...args: any[]) { return this._requestFn(...args); } } /** * retry options */ export declare type RetryOptions = { /** * the number of attempts to retry failed request, default 5 */ retries?: number, /** * minimum delay in seconds before retrying, default 1 */ minDelayInSeconds?: number, /** * maximum delay in seconds before retrying, default 30 */ maxDelayInSeconds?: number }