mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 15:34:13 +00:00 
			
		
		
		
	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
					
				
					 2 changed files with 40 additions and 23 deletions
				
			
		| 
						 | 
					@ -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 => {
 | 
				
			||||||
					throw new ApiError({
 | 
										if ('info' in err) {
 | 
				
			||||||
						message: 'Rate limit exceeded. Please try again later.',
 | 
											// errはLimiter.LimiterInfoであることが期待される
 | 
				
			||||||
						code: 'RATE_LIMIT_EXCEEDED',
 | 
											throw new ApiError({
 | 
				
			||||||
						id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
 | 
												message: 'Rate limit exceeded. Please try again later.',
 | 
				
			||||||
						httpStatusCode: 429,
 | 
												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.');
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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…
	
	Add table
		
		Reference in a new issue