summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKisaragi <48310258+KisaragiEffective@users.noreply.github.com>2024-06-13 10:56:26 +0900
committerGitHub <noreply@github.com>2024-06-13 10:56:26 +0900
commitdc3629e732e5aefd792452f0b43a7bb7fdaf103e (patch)
treec10692a00f80c034498d76967d480be9f9104f95
parentnode 22 support (diff)
downloadsharkey-dc3629e732e5aefd792452f0b43a7bb7fdaf103e.tar.gz
sharkey-dc3629e732e5aefd792452f0b43a7bb7fdaf103e.tar.bz2
sharkey-dc3629e732e5aefd792452f0b43a7bb7fdaf103e.zip
feat(backend): report `Retry-After` if client hit rate limit (#13949)
* feat(backend): report `Retry-After` if client hit rate limit * refactor(backend): fix lint error
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts27
-rw-r--r--packages/backend/src/server/api/RateLimiterService.ts36
2 files changed, 40 insertions, 23 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 166f9c8675..47f64f6609 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
+ } else if (err.code === 'RATE_LIMIT_EXCEEDED') {
+ const info: unknown = err.info;
+ const unixEpochInSeconds = Date.now();
+ if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
+ const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
+ // もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
+ reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
+ } else {
+ this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
+ }
} else if (!statusCode) {
statusCode = 500;
}
@@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- });
+ if ('info' in err) {
+ // errはLimiter.LimiterInfoであることが期待される
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, err.info);
+ } else {
+ throw new TypeError('information must be a rate-limiter information.');
+ }
});
}
}
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index 0439cdfe5e..cae106c273 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -32,11 +32,13 @@ export class RateLimiterService {
@bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
- return new Promise<void>((ok, reject) => {
- if (this.disabled) ok();
+ {
+ if (this.disabled) {
+ return Promise.resolve();
+ }
// Short-term limit
- const min = (): void => {
+ const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor,
@@ -46,25 +48,25 @@ export class RateLimiterService {
minIntervalLimiter.get((err, info) => {
if (err) {
- return reject('ERR');
+ return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) {
- reject('BRIEF_REQUEST_INTERVAL');
+ return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
- max();
+ return max;
} else {
- ok();
+ return ok();
}
}
});
- };
+ });
// Long term limit
- const max = (): void => {
+ const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
@@ -74,18 +76,18 @@ export class RateLimiterService {
limiter.get((err, info) => {
if (err) {
- return reject('ERR');
+ return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
- reject('RATE_LIMIT_EXCEEDED');
+ return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else {
- ok();
+ return ok();
}
});
- };
+ });
const hasShortTermLimit = typeof limitation.minInterval === 'number';
@@ -94,12 +96,12 @@ export class RateLimiterService {
typeof limitation.max === 'number';
if (hasShortTermLimit) {
- min();
+ return min;
} else if (hasLongTermLimit) {
- max();
+ return max;
} else {
- ok();
+ return Promise.resolve();
}
- });
+ }
}
}