Skip to content

Commit dc3629e

Browse files
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
1 parent c73d739 commit dc3629e

File tree

2 files changed

+40
-23
lines changed

2 files changed

+40
-23
lines changed

packages/backend/src/server/api/ApiCallService.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown {
7373
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
7474
}
7575
statusCode = statusCode ?? 403;
76+
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
77+
const info: unknown = err.info;
78+
const unixEpochInSeconds = Date.now();
79+
if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') {
80+
const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000);
81+
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
82+
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
83+
} else {
84+
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
85+
}
7686
} else if (!statusCode) {
7787
statusCode = 500;
7888
}
@@ -308,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown {
308318
if (factor > 0) {
309319
// Rate limit
310320
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
311-
throw new ApiError({
312-
message: 'Rate limit exceeded. Please try again later.',
313-
code: 'RATE_LIMIT_EXCEEDED',
314-
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
315-
httpStatusCode: 429,
316-
});
321+
if ('info' in err) {
322+
// errはLimiter.LimiterInfoであることが期待される
323+
throw new ApiError({
324+
message: 'Rate limit exceeded. Please try again later.',
325+
code: 'RATE_LIMIT_EXCEEDED',
326+
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
327+
httpStatusCode: 429,
328+
}, err.info);
329+
} else {
330+
throw new TypeError('information must be a rate-limiter information.');
331+
}
317332
});
318333
}
319334
}

packages/backend/src/server/api/RateLimiterService.ts

+19-17
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ export class RateLimiterService {
3232

3333
@bindThis
3434
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
35-
return new Promise<void>((ok, reject) => {
36-
if (this.disabled) ok();
35+
{
36+
if (this.disabled) {
37+
return Promise.resolve();
38+
}
3739

3840
// Short-term limit
39-
const min = (): void => {
41+
const min = new Promise<void>((ok, reject) => {
4042
const minIntervalLimiter = new Limiter({
4143
id: `${actor}:${limitation.key}:min`,
4244
duration: limitation.minInterval! * factor,
@@ -46,25 +48,25 @@ export class RateLimiterService {
4648

4749
minIntervalLimiter.get((err, info) => {
4850
if (err) {
49-
return reject('ERR');
51+
return reject({ code: 'ERR', info });
5052
}
5153

5254
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
5355

5456
if (info.remaining === 0) {
55-
reject('BRIEF_REQUEST_INTERVAL');
57+
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
5658
} else {
5759
if (hasLongTermLimit) {
58-
max();
60+
return max;
5961
} else {
60-
ok();
62+
return ok();
6163
}
6264
}
6365
});
64-
};
66+
});
6567

6668
// Long term limit
67-
const max = (): void => {
69+
const max = new Promise<void>((ok, reject) => {
6870
const limiter = new Limiter({
6971
id: `${actor}:${limitation.key}`,
7072
duration: limitation.duration! * factor,
@@ -74,18 +76,18 @@ export class RateLimiterService {
7476

7577
limiter.get((err, info) => {
7678
if (err) {
77-
return reject('ERR');
79+
return reject({ code: 'ERR', info });
7880
}
7981

8082
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
8183

8284
if (info.remaining === 0) {
83-
reject('RATE_LIMIT_EXCEEDED');
85+
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
8486
} else {
85-
ok();
87+
return ok();
8688
}
8789
});
88-
};
90+
});
8991

9092
const hasShortTermLimit = typeof limitation.minInterval === 'number';
9193

@@ -94,12 +96,12 @@ export class RateLimiterService {
9496
typeof limitation.max === 'number';
9597

9698
if (hasShortTermLimit) {
97-
min();
99+
return min;
98100
} else if (hasLongTermLimit) {
99-
max();
101+
return max;
100102
} else {
101-
ok();
103+
return Promise.resolve();
102104
}
103-
});
105+
}
104106
}
105107
}

0 commit comments

Comments
 (0)