Skip to content

Commit

Permalink
Reapply "feat(backend): report Retry-After if client hit rate limit (
Browse files Browse the repository at this point in the history
…misskey-dev#13949)"

This reverts commit 3aa7e78.
  • Loading branch information
kanarikanaru committed Jul 12, 2024
1 parent 7fccc95 commit 9a87746
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 23 deletions.
27 changes: 21 additions & 6 deletions packages/backend/src/server/api/ApiCallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.');
}
});
}
}
Expand Down
36 changes: 19 additions & 17 deletions packages/backend/src/server/api/RateLimiterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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';

Expand All @@ -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();
}
});
}
}
}

0 comments on commit 9a87746

Please sign in to comment.