1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FastifyReply } from 'fastify';
export type RateLimit = BucketRateLimit | LegacyRateLimit;
export type Keyed<T> = T & { key: string };
/**
* Rate limit based on "leaky bucket" logic.
* The bucket count increases with each call, and decreases gradually at a given rate.
* The subject is blocked until the bucket count drops below the limit.
*/
export interface BucketRateLimit {
/**
* Unique key identifying the particular resource (or resource group) being limited.
*/
key?: string;
/**
* Constant value identifying the type of rate limit.
*/
type: 'bucket';
/**
* Size of the bucket, in number of requests.
* The subject will be blocked when the number of calls exceeds this size.
*/
size: number;
/**
* How often the bucket should "drip" and reduce the counter, measured in milliseconds.
* Defaults to 1000 (1 second).
*/
dripRate?: number;
/**
* Amount to reduce the counter on each drip.
* Defaults to 1.
*/
dripSize?: number;
}
/**
* Legacy rate limit based on a "request window" with a maximum number of requests within a given time box.
* These will be translated into a bucket with linear drip rate.
*/
export interface LegacyRateLimit {
/**
* Unique key identifying the particular resource (or resource group) being limited.
*/
key?: string;
/**
* Constant value identifying the type of rate limit.
* Must be excluded or explicitly set to undefined
*/
type?: undefined;
/**
* Duration of the request window, in milliseconds.
* If present, then "max" must also be included.
*/
duration?: number;
/**
* Maximum number of requests allowed in the request window.
* If present, then "duration" must also be included.
*/
max?: number;
/**
* Optional minimum interval between consecutive requests.
* Will apply in addition to the primary rate limit.
*/
minInterval?: number;
}
/**
* Metadata about the current status of a rate limiter
*/
export interface LimitInfo {
/**
* True if the limit has been reached, and the call should be blocked.
*/
blocked: boolean;
/**
* Number of calls that can be made before the limit is triggered.
*/
remaining: number;
/**
* Time in seconds until the next call can be made, or zero if the next call can be made immediately.
* Rounded up to the nearest second.
*/
resetSec: number;
/**
* Time in milliseconds until the next call can be made, or zero if the next call can be made immediately.
* Rounded up to the nearest milliseconds.
*/
resetMs: number;
/**
* Time in seconds until the limit has fully reset.
* Rounded up to the nearest second.
*/
fullResetSec: number;
/**
* Time in milliseconds until the limit has fully reset.
* Rounded up to the nearest millisecond.
*/
fullResetMs: number;
}
export const disabledLimitInfo: Readonly<LimitInfo> = Object.freeze({
blocked: false,
remaining: Number.MAX_SAFE_INTEGER,
resetSec: 0,
resetMs: 0,
fullResetSec: 0,
fullResetMs: 0,
});
export function isLegacyRateLimit(limit: RateLimit): limit is LegacyRateLimit {
return limit.type === undefined;
}
export type MaxLegacyLimit = LegacyRateLimit & { duration: number, max: number };
export function hasMaxLimit(limit: LegacyRateLimit): limit is MaxLegacyLimit {
return limit.max != null && limit.duration != null;
}
export type MinLegacyLimit = LegacyRateLimit & { minInterval: number };
export function hasMinLimit(limit: LegacyRateLimit): limit is MinLegacyLimit {
return limit.minInterval != null;
}
export function sendRateLimitHeaders(reply: FastifyReply, info: LimitInfo): void {
// Number of seconds until the limit has fully reset.
const clear = (info.fullResetMs / 1000).toFixed(3);
reply.header('X-RateLimit-Clear', clear);
// Number of calls that can be made before being limited.
const remaining = info.remaining.toString();
reply.header('X-RateLimit-Remaining', remaining);
if (info.blocked) {
// Number of seconds to wait before trying again. Left for backwards compatibility.
const retry = info.resetSec.toString();
reply.header('Retry-After', retry);
// Number of milliseconds to wait before trying again.
const reset = (info.resetMs / 1000).toFixed(3);
reply.header('X-RateLimit-Reset', reset);
}
}
|