mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(slowmode): stop the local cooldown from outgrowing the channel setting (#1795)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {SlowmodeRateLimitError} from '@fluxer/errors/src/domains/core/SlowmodeRateLimitError';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
interface SlowmodeResponseBody {
|
||||
code: string;
|
||||
retry_after: number;
|
||||
}
|
||||
|
||||
async function readSlowmodeResponse(error: SlowmodeRateLimitError): Promise<{
|
||||
status: number;
|
||||
header: string | null;
|
||||
body: SlowmodeResponseBody;
|
||||
}> {
|
||||
const response = error.getResponse();
|
||||
const body = (await response.json()) as SlowmodeResponseBody;
|
||||
return {status: response.status, header: response.headers.get('Retry-After'), body};
|
||||
}
|
||||
|
||||
describe('SlowmodeRateLimitError', () => {
|
||||
it('reports Retry-After in whole seconds and the body in decimal seconds', async () => {
|
||||
const {status, header, body} = await readSlowmodeResponse(
|
||||
new SlowmodeRateLimitError({retryAfter: 5, retryAfterDecimal: 4.7}),
|
||||
);
|
||||
expect(status).toBe(400);
|
||||
expect(body.code).toBe('SLOWMODE_RATE_LIMITED');
|
||||
expect(body.retry_after).toBe(4.7);
|
||||
expect(header).toBe('5');
|
||||
});
|
||||
|
||||
it('keeps the header and the body within one second of each other', async () => {
|
||||
const {header, body} = await readSlowmodeResponse(
|
||||
new SlowmodeRateLimitError({retryAfter: 5, retryAfterDecimal: 4.997}),
|
||||
);
|
||||
const headerSeconds = Number(header);
|
||||
expect(headerSeconds - body.retry_after).toBeLessThan(1);
|
||||
expect(headerSeconds).toBeGreaterThanOrEqual(body.retry_after);
|
||||
});
|
||||
|
||||
it('falls back to one second when the caller has no retry window', async () => {
|
||||
const {header, body} = await readSlowmodeResponse(new SlowmodeRateLimitError({retryAfter: undefined}));
|
||||
expect(header).toBe('1');
|
||||
expect(body.retry_after).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {
|
||||
sanitizeRetryAfterDecimalSeconds,
|
||||
sanitizeRetryAfterSeconds,
|
||||
} from '@fluxer/errors/src/domains/core/RetryAfterSeconds';
|
||||
import {ThrottledError} from '@fluxer/errors/src/domains/core/ThrottledError';
|
||||
import type {FluxerErrorData} from '@fluxer/errors/src/FluxerError';
|
||||
|
||||
type RateLimitScope = 'global' | 'shared' | 'user';
|
||||
|
||||
function sanitizeRetryAfter(value: number | undefined | null): number {
|
||||
if (value == null || !Number.isFinite(value) || value < 0) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.ceil(value));
|
||||
}
|
||||
|
||||
function sanitizeRetryAfterDecimal(value: number | undefined | null, fallback: number): number {
|
||||
if (value == null || !Number.isFinite(value) || value < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(0.001, value);
|
||||
}
|
||||
|
||||
function sanitizeResetTime(resetTime: Date): number {
|
||||
const timestamp = resetTime.getTime();
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
@@ -64,8 +54,8 @@ export class RateLimitError extends ThrottledError {
|
||||
bucketHash?: string;
|
||||
scope?: RateLimitScope;
|
||||
}) {
|
||||
const safeRetryAfter = sanitizeRetryAfter(retryAfter);
|
||||
const safeRetryAfterDecimal = sanitizeRetryAfterDecimal(retryAfterDecimal, safeRetryAfter);
|
||||
const safeRetryAfter = sanitizeRetryAfterSeconds(retryAfter);
|
||||
const safeRetryAfterDecimal = sanitizeRetryAfterDecimalSeconds(retryAfterDecimal, safeRetryAfter);
|
||||
const safeResetTimestamp = sanitizeResetTime(resetTime);
|
||||
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 1;
|
||||
const safeResetAfterDecimal =
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export function sanitizeRetryAfterSeconds(value: number | undefined | null): number {
|
||||
if (value == null || !Number.isFinite(value) || value < 0) {
|
||||
return 1;
|
||||
}
|
||||
return Math.max(1, Math.ceil(value));
|
||||
}
|
||||
|
||||
export function sanitizeRetryAfterDecimalSeconds(value: number | undefined | null, fallback: number): number {
|
||||
if (value == null || !Number.isFinite(value) || value < 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(0.001, value);
|
||||
}
|
||||
@@ -2,22 +2,28 @@
|
||||
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {BadRequestError} from '@fluxer/errors/src/domains/core/BadRequestError';
|
||||
import {
|
||||
sanitizeRetryAfterDecimalSeconds,
|
||||
sanitizeRetryAfterSeconds,
|
||||
} from '@fluxer/errors/src/domains/core/RetryAfterSeconds';
|
||||
|
||||
export class SlowmodeRateLimitError extends BadRequestError {
|
||||
constructor({
|
||||
retryAfter,
|
||||
retryAfterDecimal,
|
||||
}: {
|
||||
retryAfter: number;
|
||||
retryAfter: number | undefined;
|
||||
retryAfterDecimal?: number;
|
||||
}) {
|
||||
const safeRetryAfter = sanitizeRetryAfterSeconds(retryAfter);
|
||||
const safeRetryAfterDecimal = sanitizeRetryAfterDecimalSeconds(retryAfterDecimal, safeRetryAfter);
|
||||
super({
|
||||
code: APIErrorCodes.SLOWMODE_RATE_LIMITED,
|
||||
data: {
|
||||
retry_after: retryAfterDecimal ?? retryAfter,
|
||||
retry_after: safeRetryAfterDecimal,
|
||||
},
|
||||
headers: {
|
||||
'Retry-After': retryAfter.toString(),
|
||||
'Retry-After': safeRetryAfter.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user