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
text/typescript
;
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
}