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
This commit is contained in:
parent
c73d739bd6
commit
dc3629e732
|
@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
|
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
|
||||||
}
|
}
|
||||||
statusCode = statusCode ?? 403;
|
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) {
|
} else if (!statusCode) {
|
||||||
statusCode = 500;
|
statusCode = 500;
|
||||||
}
|
}
|
||||||
|
@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
if (factor > 0) {
|
if (factor > 0) {
|
||||||
// Rate limit
|
// Rate limit
|
||||||
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
|
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
|
||||||
|
if ('info' in err) {
|
||||||
|
// errはLimiter.LimiterInfoであることが期待される
|
||||||
throw new ApiError({
|
throw new ApiError({
|
||||||
message: 'Rate limit exceeded. Please try again later.',
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
code: 'RATE_LIMIT_EXCEEDED',
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||||
httpStatusCode: 429,
|
httpStatusCode: 429,
|
||||||
});
|
}, err.info);
|
||||||
|
} else {
|
||||||
|
throw new TypeError('information must be a rate-limiter information.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,11 +32,13 @@ export class RateLimiterService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
|
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
|
// Short-term limit
|
||||||
const min = (): void => {
|
const min = new Promise<void>((ok, reject) => {
|
||||||
const minIntervalLimiter = new Limiter({
|
const minIntervalLimiter = new Limiter({
|
||||||
id: `${actor}:${limitation.key}:min`,
|
id: `${actor}:${limitation.key}:min`,
|
||||||
duration: limitation.minInterval! * factor,
|
duration: limitation.minInterval! * factor,
|
||||||
|
@ -46,25 +48,25 @@ export class RateLimiterService {
|
||||||
|
|
||||||
minIntervalLimiter.get((err, info) => {
|
minIntervalLimiter.get((err, info) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject('ERR');
|
return reject({ code: 'ERR', info });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
||||||
|
|
||||||
if (info.remaining === 0) {
|
if (info.remaining === 0) {
|
||||||
reject('BRIEF_REQUEST_INTERVAL');
|
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
|
||||||
} else {
|
} else {
|
||||||
if (hasLongTermLimit) {
|
if (hasLongTermLimit) {
|
||||||
max();
|
return max;
|
||||||
} else {
|
} else {
|
||||||
ok();
|
return ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
// Long term limit
|
// Long term limit
|
||||||
const max = (): void => {
|
const max = new Promise<void>((ok, reject) => {
|
||||||
const limiter = new Limiter({
|
const limiter = new Limiter({
|
||||||
id: `${actor}:${limitation.key}`,
|
id: `${actor}:${limitation.key}`,
|
||||||
duration: limitation.duration! * factor,
|
duration: limitation.duration! * factor,
|
||||||
|
@ -74,18 +76,18 @@ export class RateLimiterService {
|
||||||
|
|
||||||
limiter.get((err, info) => {
|
limiter.get((err, info) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject('ERR');
|
return reject({ code: 'ERR', info });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
||||||
|
|
||||||
if (info.remaining === 0) {
|
if (info.remaining === 0) {
|
||||||
reject('RATE_LIMIT_EXCEEDED');
|
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
|
||||||
} else {
|
} else {
|
||||||
ok();
|
return ok();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||||
|
|
||||||
|
@ -94,12 +96,12 @@ export class RateLimiterService {
|
||||||
typeof limitation.max === 'number';
|
typeof limitation.max === 'number';
|
||||||
|
|
||||||
if (hasShortTermLimit) {
|
if (hasShortTermLimit) {
|
||||||
min();
|
return min;
|
||||||
} else if (hasLongTermLimit) {
|
} else if (hasLongTermLimit) {
|
||||||
max();
|
return max;
|
||||||
} else {
|
} else {
|
||||||
ok();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue