refactor: return rate limit instead of throwing an object

This commit is contained in:
anatawa12 2024-06-22 13:34:52 +09:00
parent fce656b5e9
commit f9dd3aca78
No known key found for this signature in database
GPG Key ID: 9CA909848B8E4EA6
3 changed files with 26 additions and 21 deletions

View File

@ -317,19 +317,15 @@ 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 => { const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
if ('info' in err) { if (rateLimit != null) {
// 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); }, rateLimit.info);
} else {
throw new TypeError('information must be a rate-limiter information.');
} }
});
} }
} }

View File

@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js'; import type { IEndpointMeta } from './endpoints.js';
type RateLimitInfo = {
code: 'BRIEF_REQUEST_INTERVAL',
info: Limiter.LimiterInfo,
} | {
code: 'RATE_LIMIT_EXCEEDED',
info: Limiter.LimiterInfo,
}
@Injectable() @Injectable()
export class RateLimiterService { export class RateLimiterService {
private logger: Logger; private logger: Logger;
@ -35,7 +43,7 @@ export class RateLimiterService {
return new Promise<Limiter.LimiterInfo>((resolve, reject) => { return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
new Limiter(options).get((err, info) => { new Limiter(options).get((err, info) => {
if (err) { if (err) {
return reject({ code: 'ERR', info }); return reject(err);
} }
resolve(info); resolve(info);
}); });
@ -43,9 +51,9 @@ export class RateLimiterService {
} }
@bindThis @bindThis
public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) { public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
if (this.disabled) { if (this.disabled) {
return; return null;
} }
// Short-term limit // Short-term limit
@ -61,7 +69,7 @@ export class RateLimiterService {
if (info.remaining === 0) { if (info.remaining === 0) {
// eslint-disable-next-line no-throw-literal // eslint-disable-next-line no-throw-literal
throw { code: 'BRIEF_REQUEST_INTERVAL', info }; return { code: 'BRIEF_REQUEST_INTERVAL', info };
} }
} }
@ -78,8 +86,10 @@ export class RateLimiterService {
if (info.remaining === 0) { if (info.remaining === 0) {
// eslint-disable-next-line no-throw-literal // eslint-disable-next-line no-throw-literal
throw { code: 'RATE_LIMIT_EXCEEDED', info }; return { code: 'RATE_LIMIT_EXCEEDED', info };
} }
} }
return null
} }
} }

View File

@ -73,10 +73,9 @@ export class SigninApiService {
return { error }; return { error };
} }
try {
// not more than 1 attempt per second and not more than 10 attempts per hour // not more than 1 attempt per second and not more than 10 attempts per hour
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
} catch (err) { if (rateLimit != null) {
reply.code(429); reply.code(429);
return { return {
error: { error: {