Skip to content

Commit

Permalink
Merge pull request #235 from secretkeylabs/vic/rate-limit-fix
Browse files Browse the repository at this point in the history
Replace the axios rate limiter lib with own
  • Loading branch information
victorkirov authored Oct 28, 2024
2 parents 35aba9e + c10ae9e commit e847604
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 15 deletions.
21 changes: 19 additions & 2 deletions api/esplora/esploraAPiProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import rateLimit from 'axios-rate-limit';
import axiosRetry from 'axios-retry';
import { XVERSE_BTC_BASE_URI_MAINNET, XVERSE_BTC_BASE_URI_SIGNET, XVERSE_BTC_BASE_URI_TESTNET } from '../../constant';
import {
Expand All @@ -12,6 +11,7 @@ import {
TransactionOutspend,
UTXO,
} from '../../types';
import { AxiosRateLimit } from '../../utils/axiosRateLimit';

export interface EsploraApiProviderOptions {
network: NetworkType;
Expand All @@ -22,8 +22,12 @@ export interface EsploraApiProviderOptions {
export class BitcoinEsploraApiProvider {
bitcoinApi: AxiosInstance;

rateLimiter: AxiosRateLimit;

fallbackBitcoinApi?: AxiosInstance;

fallbackRateLimiter?: AxiosRateLimit;

_network: NetworkType;

constructor(options: EsploraApiProviderOptions) {
Expand All @@ -48,17 +52,30 @@ export class BitcoinEsploraApiProvider {
const axiosConfig: AxiosRequestConfig = { baseURL };

this._network = network;
this.bitcoinApi = rateLimit(axios.create(axiosConfig), {
this.bitcoinApi = axios.create(axiosConfig);

this.rateLimiter = new AxiosRateLimit(this.bitcoinApi, {
maxRPS: 10,
});

axiosRetry(this.bitcoinApi, {
retries: 1,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
if (error?.response?.status === 429 || (error?.response?.status ?? 0) >= 500) {
return true;
}
return false;
},
});

if (fallbackUrl) {
this.fallbackBitcoinApi = axios.create({ ...axiosConfig, baseURL: fallbackUrl });

this.fallbackRateLimiter = new AxiosRateLimit(this.fallbackBitcoinApi, {
maxRPS: 10,
});

this.bitcoinApi.interceptors.response.use(
// if the request succeeds, we do nothing.
(response) => response,
Expand Down
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@zondax/ledger-stacks": "^1.0.4",
"async-mutex": "^0.4.0",
"axios": "1.7.7",
"axios-rate-limit": "1.4.0",
"axios-retry": "4.5.0",
"base64url": "^3.0.1",
"bip32": "^4.0.0",
Expand Down
98 changes: 98 additions & 0 deletions utils/axiosRateLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* inspired by axios-rate-limit with a few adjustments and bug fixes
* https://github.com/aishek/axios-rate-limit
*/

import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

type RateLimitRequestHandler = {
resolve: () => boolean;
};

type RateLimitOptions =
| {
maxRPS: number;
}
| {
maxRequests: number;
perMilliseconds: number;
};

function throwIfCancellationRequested(config: InternalAxiosRequestConfig<any>) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

export class AxiosRateLimit {
private queue: RateLimitRequestHandler[];

private timeslotRequests: number;

private perMilliseconds: number;

private maxRequests: number;

constructor(axios: AxiosInstance, options: RateLimitOptions) {
this.queue = [];
this.timeslotRequests = 0;

if ('maxRPS' in options) {
this.perMilliseconds = 1000;
this.maxRequests = options.maxRPS;
} else {
this.perMilliseconds = options.perMilliseconds;
this.maxRequests = options.maxRequests;
}

function handleError(error: unknown) {
return Promise.reject(error);
}

axios.interceptors.request.use(this.handleRequest, handleError);
}

private handleRequest = (request: InternalAxiosRequestConfig<any>) => {
return new Promise<InternalAxiosRequestConfig<any>>((resolve, reject) => {
this.push({
resolve: function () {
try {
throwIfCancellationRequested(request);
} catch (error) {
reject(error);
return false;
}
resolve(request);
return true;
},
});
});
};

private push = (requestHandler: RateLimitRequestHandler) => {
this.queue.push(requestHandler);
this.shift();
};

private onRequestTimerMet = () => {
this.timeslotRequests--;
this.shift();
};

private shift = () => {
if (this.timeslotRequests >= this.maxRequests) return;

const queued = this.queue.shift();
if (!queued) return;

const resolved = queued.resolve();

if (!resolved) {
this.shift(); // rejected request --> shift another request
return;
}

this.timeslotRequests++;
setTimeout(this.onRequestTimerMet, this.perMilliseconds);
};
}

0 comments on commit e847604

Please sign in to comment.