From f9dd3aca785dc9cec5c4f2a7abc746987b534e1a Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Sat, 22 Jun 2024 13:34:52 +0900
Subject: [PATCH] refactor: return rate limit instead of throwing an object

---
 .../backend/src/server/api/ApiCallService.ts  | 22 ++++++++-----------
 .../src/server/api/RateLimiterService.ts      | 20 ++++++++++++-----
 .../src/server/api/SigninApiService.ts        |  5 ++---
 3 files changed, 26 insertions(+), 21 deletions(-)

diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 47f64f6609..02a1df12b8 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -317,19 +317,15 @@ 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 => {
-					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.');
-					}
-				});
+				const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
+				if (rateLimit != null) {
+					throw new ApiError({
+						message: 'Rate limit exceeded. Please try again later.',
+						code: 'RATE_LIMIT_EXCEEDED',
+						id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+						httpStatusCode: 429,
+					}, rateLimit.info);
+				}
 			}
 		}
 
diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts
index f78b380d9e..9b1cef8155 100644
--- a/packages/backend/src/server/api/RateLimiterService.ts
+++ b/packages/backend/src/server/api/RateLimiterService.ts
@@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
 import type { IEndpointMeta } from './endpoints.js';
 
+type RateLimitInfo = {
+	code: 'BRIEF_REQUEST_INTERVAL',
+	info: Limiter.LimiterInfo,
+} | {
+	code: 'RATE_LIMIT_EXCEEDED',
+	info: Limiter.LimiterInfo,
+}
+
 @Injectable()
 export class RateLimiterService {
 	private logger: Logger;
@@ -35,7 +43,7 @@ export class RateLimiterService {
 		return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
 			new Limiter(options).get((err, info) => {
 				if (err) {
-					return reject({ code: 'ERR', info });
+					return reject(err);
 				}
 				resolve(info);
 			});
@@ -43,9 +51,9 @@ export class RateLimiterService {
 	}
 
 	@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) {
-			return;
+			return null;
 		}
 
 		// Short-term limit
@@ -61,7 +69,7 @@ export class RateLimiterService {
 
 			if (info.remaining === 0) {
 				// 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) {
 				// eslint-disable-next-line no-throw-literal
-				throw { code: 'RATE_LIMIT_EXCEEDED', info };
+				return { code: 'RATE_LIMIT_EXCEEDED', info };
 			}
 		}
+
+		return null
 	}
 }
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index edac9b3beb..d8e96006ec 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -73,10 +73,9 @@ export class SigninApiService {
 			return { error };
 		}
 
-		try {
 		// 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));
-		} catch (err) {
+		const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+		if (rateLimit != null) {
 			reply.code(429);
 			return {
 				error: {