@atproto/xrpc-server 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/rate-limiter-http.d.ts +24 -0
- package/dist/rate-limiter-http.d.ts.map +1 -0
- package/dist/rate-limiter-http.js +49 -0
- package/dist/rate-limiter-http.js.map +1 -0
- package/dist/rate-limiter.d.ts +32 -42
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +29 -75
- package/dist/rate-limiter.js.map +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -7
- package/dist/server.js.map +1 -1
- package/dist/stream/frames.d.ts +3 -3
- package/dist/stream/frames.d.ts.map +1 -1
- package/dist/stream/logger.d.ts.map +1 -1
- package/dist/stream/server.d.ts.map +1 -1
- package/dist/stream/stream.d.ts +1 -1
- package/dist/stream/subscription.d.ts.map +1 -1
- package/dist/stream/subscription.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +15 -16
- package/src/rate-limiter-http.ts +82 -0
- package/src/rate-limiter.ts +68 -156
- package/src/server.ts +49 -13
- package/tsconfig.build.json +2 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.tests.json +2 -2
package/src/rate-limiter.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
1
|
import {
|
|
3
2
|
RateLimiterAbstract,
|
|
4
3
|
RateLimiterMemory,
|
|
@@ -6,43 +5,29 @@ import {
|
|
|
6
5
|
RateLimiterRes,
|
|
7
6
|
} from 'rate-limiter-flexible'
|
|
8
7
|
import { ResponseType, XRPCError } from './errors.js'
|
|
9
|
-
import { logger } from './logger.js'
|
|
10
8
|
|
|
11
9
|
// @NOTE Do not depend (directly or indirectly) on "./types" here, as it would
|
|
12
10
|
// create a circular dependency.
|
|
13
11
|
|
|
14
|
-
export
|
|
15
|
-
req: IncomingMessage
|
|
16
|
-
res?: ServerResponse
|
|
17
|
-
}
|
|
12
|
+
export type { RateLimiterAbstract }
|
|
18
13
|
|
|
19
|
-
export type CalcKeyFn<C
|
|
20
|
-
|
|
21
|
-
) => string | null
|
|
22
|
-
export type CalcPointsFn<C extends RateLimiterContext = RateLimiterContext> = (
|
|
23
|
-
ctx: C,
|
|
24
|
-
) => number
|
|
14
|
+
export type CalcKeyFn<C = unknown> = (ctx: C) => string | null
|
|
15
|
+
export type CalcPointsFn<C = unknown> = (ctx: C) => number
|
|
25
16
|
|
|
26
|
-
export interface RateLimiterI<
|
|
27
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
28
|
-
> {
|
|
17
|
+
export interface RateLimiterI<C = unknown> {
|
|
29
18
|
consume: RateLimiterConsume<C>
|
|
30
19
|
reset: RateLimiterReset<C>
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
export type RateLimiterConsumeOptions<
|
|
34
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
35
|
-
> = {
|
|
22
|
+
export type RateLimiterConsumeOptions<C = unknown> = {
|
|
36
23
|
calcKey?: CalcKeyFn<C>
|
|
37
24
|
calcPoints?: CalcPointsFn<C>
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
export type RateLimiterConsume<
|
|
41
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
42
|
-
> = (
|
|
27
|
+
export type RateLimiterConsume<C = unknown> = (
|
|
43
28
|
ctx: C,
|
|
44
29
|
opts?: RateLimiterConsumeOptions<C>,
|
|
45
|
-
) => Promise<RateLimiterStatus |
|
|
30
|
+
) => Promise<RateLimiterStatus | null>
|
|
46
31
|
|
|
47
32
|
export type RateLimiterStatus = {
|
|
48
33
|
limit: number
|
|
@@ -53,31 +38,41 @@ export type RateLimiterStatus = {
|
|
|
53
38
|
isFirstInDuration: boolean
|
|
54
39
|
}
|
|
55
40
|
|
|
56
|
-
export type RateLimiterResetOptions<
|
|
57
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
58
|
-
> = {
|
|
41
|
+
export type RateLimiterResetOptions<C = unknown> = {
|
|
59
42
|
calcKey?: CalcKeyFn<C>
|
|
60
43
|
}
|
|
61
44
|
|
|
62
|
-
export type RateLimiterReset<
|
|
63
|
-
C
|
|
64
|
-
|
|
45
|
+
export type RateLimiterReset<C = unknown> = (
|
|
46
|
+
ctx: C,
|
|
47
|
+
opts?: RateLimiterResetOptions<C>,
|
|
48
|
+
) => Promise<void>
|
|
65
49
|
|
|
66
|
-
export type
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
export type RateLimiterErrorHandlerDetails = {
|
|
51
|
+
key: string
|
|
52
|
+
points: number
|
|
53
|
+
limiter: {
|
|
54
|
+
keyPrefix: string
|
|
55
|
+
points: number
|
|
56
|
+
duration: number
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export type RateLimiterErrorHandler<C = unknown> = (
|
|
60
|
+
err: unknown,
|
|
61
|
+
ctx: C,
|
|
62
|
+
details: RateLimiterErrorHandlerDetails,
|
|
63
|
+
) => Promise<RateLimiterStatus | null>
|
|
64
|
+
|
|
65
|
+
export type RateLimiterOptions<C = unknown> = {
|
|
69
66
|
keyPrefix: string
|
|
70
67
|
durationMs: number
|
|
71
68
|
points: number
|
|
72
69
|
calcKey: CalcKeyFn<C>
|
|
73
70
|
calcPoints: CalcPointsFn<C>
|
|
74
|
-
|
|
71
|
+
onError?: RateLimiterErrorHandler<C>
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
export class RateLimiter<C
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
private readonly failClosed?: boolean
|
|
74
|
+
export class RateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
75
|
+
private readonly onError?: RateLimiterErrorHandler<C>
|
|
81
76
|
private readonly calcKey: CalcKeyFn<C>
|
|
82
77
|
private readonly calcPoints: CalcPointsFn<C>
|
|
83
78
|
|
|
@@ -86,7 +81,7 @@ export class RateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
|
86
81
|
options: RateLimiterOptions<C>,
|
|
87
82
|
) {
|
|
88
83
|
this.limiter = limiter
|
|
89
|
-
this.
|
|
84
|
+
this.onError = options.onError
|
|
90
85
|
this.calcKey = options.calcKey
|
|
91
86
|
this.calcPoints = options.calcPoints
|
|
92
87
|
}
|
|
@@ -94,7 +89,7 @@ export class RateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
|
94
89
|
async consume(
|
|
95
90
|
ctx: C,
|
|
96
91
|
opts?: RateLimiterConsumeOptions<C>,
|
|
97
|
-
): Promise<RateLimiterStatus |
|
|
92
|
+
): Promise<RateLimiterStatus | null> {
|
|
98
93
|
const calcKey = opts?.calcKey ?? this.calcKey
|
|
99
94
|
const key = calcKey(ctx)
|
|
100
95
|
if (key === null) {
|
|
@@ -105,28 +100,26 @@ export class RateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
|
105
100
|
if (points < 1) {
|
|
106
101
|
return null
|
|
107
102
|
}
|
|
103
|
+
const { limiter } = this
|
|
108
104
|
try {
|
|
109
|
-
const res = await
|
|
110
|
-
return formatLimiterStatus(
|
|
105
|
+
const res = await limiter.consume(key, points)
|
|
106
|
+
return formatLimiterStatus(limiter, res)
|
|
111
107
|
} catch (err) {
|
|
112
|
-
// yes this library rejects with a res not an error
|
|
113
108
|
if (err instanceof RateLimiterRes) {
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
// Wrap rate-limiter-flexible error into our own error type
|
|
110
|
+
const status = formatLimiterStatus(limiter, err)
|
|
111
|
+
throw new RateLimitExceededError(status)
|
|
112
|
+
} else if (err instanceof RateLimitExceededError) {
|
|
113
|
+
// Propagate RateLimitExceededError errors
|
|
114
|
+
throw err
|
|
116
115
|
} else {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
points: this.limiter.points,
|
|
125
|
-
duration: this.limiter.duration,
|
|
126
|
-
},
|
|
127
|
-
'rate limiter failed to consume points',
|
|
128
|
-
)
|
|
129
|
-
return null
|
|
116
|
+
// Most likely a system error (failed to connect to Redis, etc). Allow
|
|
117
|
+
// the caller to decide how to handle it (fail open by allowing the
|
|
118
|
+
// request, fail closed by rejecting the request, etc).
|
|
119
|
+
const { onError } = this
|
|
120
|
+
if (onError) return onError(err, ctx, { key, points, limiter })
|
|
121
|
+
|
|
122
|
+
throw err
|
|
130
123
|
}
|
|
131
124
|
}
|
|
132
125
|
}
|
|
@@ -145,9 +138,7 @@ export class RateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
|
145
138
|
}
|
|
146
139
|
}
|
|
147
140
|
|
|
148
|
-
export class MemoryRateLimiter<
|
|
149
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
150
|
-
> extends RateLimiter<C> {
|
|
141
|
+
export class MemoryRateLimiter<C = unknown> extends RateLimiter<C> {
|
|
151
142
|
constructor(options: RateLimiterOptions<C>) {
|
|
152
143
|
const limiter = new RateLimiterMemory({
|
|
153
144
|
keyPrefix: options.keyPrefix,
|
|
@@ -158,9 +149,7 @@ export class MemoryRateLimiter<
|
|
|
158
149
|
}
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
export class RedisRateLimiter<
|
|
162
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
163
|
-
> extends RateLimiter<C> {
|
|
152
|
+
export class RedisRateLimiter<C = unknown> extends RateLimiter<C> {
|
|
164
153
|
constructor(storeClient: unknown, options: RateLimiterOptions<C>) {
|
|
165
154
|
const limiter = new RateLimiterRedis({
|
|
166
155
|
storeClient,
|
|
@@ -186,9 +175,7 @@ export const formatLimiterStatus = (
|
|
|
186
175
|
}
|
|
187
176
|
}
|
|
188
177
|
|
|
189
|
-
export type WrappedRateLimiterOptions<
|
|
190
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
191
|
-
> = {
|
|
178
|
+
export type WrappedRateLimiterOptions<C = unknown> = {
|
|
192
179
|
calcKey?: CalcKeyFn<C>
|
|
193
180
|
calcPoints?: CalcPointsFn<C>
|
|
194
181
|
}
|
|
@@ -197,10 +184,7 @@ export type WrappedRateLimiterOptions<
|
|
|
197
184
|
* Wraps a {@link RateLimiterI} instance with custom key and points calculation
|
|
198
185
|
* functions.
|
|
199
186
|
*/
|
|
200
|
-
export class WrappedRateLimiter<
|
|
201
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
202
|
-
> implements RateLimiterI<C>
|
|
203
|
-
{
|
|
187
|
+
export class WrappedRateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
204
188
|
private constructor(
|
|
205
189
|
private readonly rateLimiter: RateLimiterI<C>,
|
|
206
190
|
private readonly options: Readonly<WrappedRateLimiterOptions<C>>,
|
|
@@ -219,7 +203,7 @@ export class WrappedRateLimiter<
|
|
|
219
203
|
})
|
|
220
204
|
}
|
|
221
205
|
|
|
222
|
-
static from<C
|
|
206
|
+
static from<C = unknown>(
|
|
223
207
|
rateLimiter: RateLimiterI<C>,
|
|
224
208
|
{ calcKey, calcPoints }: WrappedRateLimiterOptions<C> = {},
|
|
225
209
|
): RateLimiterI<C> {
|
|
@@ -234,10 +218,7 @@ export class WrappedRateLimiter<
|
|
|
234
218
|
* The combined rate limiter will return the tightest (most restrictive) of all
|
|
235
219
|
* the provided rate limiters.
|
|
236
220
|
*/
|
|
237
|
-
export class CombinedRateLimiter<
|
|
238
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
239
|
-
> implements RateLimiterI<C>
|
|
240
|
-
{
|
|
221
|
+
export class CombinedRateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
241
222
|
private constructor(
|
|
242
223
|
private readonly rateLimiters: readonly RateLimiterI<C>[],
|
|
243
224
|
) {}
|
|
@@ -245,7 +226,18 @@ export class CombinedRateLimiter<
|
|
|
245
226
|
async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {
|
|
246
227
|
const promises: ReturnType<RateLimiterConsume>[] = []
|
|
247
228
|
for (const rl of this.rateLimiters) promises.push(rl.consume(ctx, opts))
|
|
248
|
-
|
|
229
|
+
const results = await Promise.all(promises)
|
|
230
|
+
|
|
231
|
+
// Compute the tightest rate limit status (the one with the least remaining points)
|
|
232
|
+
let lowest: RateLimiterStatus | null = null
|
|
233
|
+
for (const resp of results) {
|
|
234
|
+
if (resp === null) continue
|
|
235
|
+
if (lowest === null || resp.remainingPoints < lowest.remainingPoints) {
|
|
236
|
+
lowest = resp
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return lowest
|
|
249
241
|
}
|
|
250
242
|
|
|
251
243
|
async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {
|
|
@@ -254,7 +246,7 @@ export class CombinedRateLimiter<
|
|
|
254
246
|
await Promise.all(promises)
|
|
255
247
|
}
|
|
256
248
|
|
|
257
|
-
static from<C
|
|
249
|
+
static from<C = unknown>(
|
|
258
250
|
rateLimiters: readonly RateLimiterI<C>[],
|
|
259
251
|
): RateLimiterI<C> | undefined {
|
|
260
252
|
if (rateLimiters.length === 0) return undefined
|
|
@@ -263,86 +255,6 @@ export class CombinedRateLimiter<
|
|
|
263
255
|
}
|
|
264
256
|
}
|
|
265
257
|
|
|
266
|
-
const getTightestLimit = (
|
|
267
|
-
resps: (RateLimiterStatus | RateLimitExceededError | null)[],
|
|
268
|
-
): RateLimiterStatus | RateLimitExceededError | null => {
|
|
269
|
-
let lowest: RateLimiterStatus | null = null
|
|
270
|
-
for (const resp of resps) {
|
|
271
|
-
if (resp === null) continue
|
|
272
|
-
if (resp instanceof RateLimitExceededError) return resp
|
|
273
|
-
if (lowest === null || resp.remainingPoints < lowest.remainingPoints) {
|
|
274
|
-
lowest = resp
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return lowest
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export type RouteRateLimiterOptions<
|
|
281
|
-
C extends RateLimiterContext = RateLimiterContext,
|
|
282
|
-
> = {
|
|
283
|
-
bypass?: (ctx: C) => boolean
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Wraps a {@link RateLimiterI} interface into a class that will apply the
|
|
288
|
-
* appropriate headers to the response if a limit is exceeded.
|
|
289
|
-
*/
|
|
290
|
-
export class RouteRateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
291
|
-
implements RateLimiterI<C>
|
|
292
|
-
{
|
|
293
|
-
constructor(
|
|
294
|
-
private readonly rateLimiter: RateLimiterI<C>,
|
|
295
|
-
private readonly options: Readonly<RouteRateLimiterOptions<C>> = {},
|
|
296
|
-
) {}
|
|
297
|
-
|
|
298
|
-
async handle(ctx: C): Promise<RateLimiterStatus | null> {
|
|
299
|
-
const { bypass } = this.options
|
|
300
|
-
if (bypass && bypass(ctx)) {
|
|
301
|
-
return null
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const result = await this.consume(ctx)
|
|
305
|
-
if (result instanceof RateLimitExceededError) {
|
|
306
|
-
setStatusHeaders(ctx, result.status)
|
|
307
|
-
throw result
|
|
308
|
-
} else if (result != null) {
|
|
309
|
-
setStatusHeaders(ctx, result)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return result
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async consume(...args: Parameters<RateLimiterConsume<C>>) {
|
|
316
|
-
return this.rateLimiter.consume(...args)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async reset(...args: Parameters<RateLimiterReset<C>>) {
|
|
320
|
-
return this.rateLimiter.reset(...args)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
static from<C extends RateLimiterContext = RateLimiterContext>(
|
|
324
|
-
rateLimiters: readonly RateLimiterI<C>[],
|
|
325
|
-
{ bypass }: RouteRateLimiterOptions<C> = {},
|
|
326
|
-
): RouteRateLimiter<C> | undefined {
|
|
327
|
-
const rateLimiter = CombinedRateLimiter.from(rateLimiters)
|
|
328
|
-
if (!rateLimiter) return undefined
|
|
329
|
-
|
|
330
|
-
return new RouteRateLimiter(rateLimiter, { bypass })
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function setStatusHeaders<C extends RateLimiterContext = RateLimiterContext>(
|
|
335
|
-
ctx: C,
|
|
336
|
-
status: RateLimiterStatus,
|
|
337
|
-
) {
|
|
338
|
-
const resetAt = Math.floor((Date.now() + status.msBeforeNext) / 1e3)
|
|
339
|
-
|
|
340
|
-
ctx.res?.setHeader('RateLimit-Limit', status.limit)
|
|
341
|
-
ctx.res?.setHeader('RateLimit-Reset', resetAt)
|
|
342
|
-
ctx.res?.setHeader('RateLimit-Remaining', status.remainingPoints)
|
|
343
|
-
ctx.res?.setHeader('RateLimit-Policy', `${status.limit};w=${status.duration}`)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
258
|
export class RateLimitExceededError extends XRPCError {
|
|
347
259
|
constructor(
|
|
348
260
|
public status: RateLimiterStatus,
|
package/src/server.ts
CHANGED
|
@@ -27,12 +27,13 @@ import {
|
|
|
27
27
|
excludeErrorResult,
|
|
28
28
|
} from './errors.js'
|
|
29
29
|
import log, { LOGGER_NAME } from './logger.js'
|
|
30
|
+
import { HttpRateLimiter } from './rate-limiter-http.js'
|
|
30
31
|
import {
|
|
31
32
|
CalcKeyFn,
|
|
32
33
|
CalcPointsFn,
|
|
34
|
+
RateLimiterErrorHandlerDetails,
|
|
33
35
|
RateLimiterI,
|
|
34
36
|
RateLimiterOptions,
|
|
35
|
-
RouteRateLimiter,
|
|
36
37
|
WrappedRateLimiter,
|
|
37
38
|
} from './rate-limiter.js'
|
|
38
39
|
import {
|
|
@@ -99,7 +100,7 @@ export class Server {
|
|
|
99
100
|
subscriptions = new Map<string, XrpcStreamServer>()
|
|
100
101
|
lex = new Lexicons()
|
|
101
102
|
options: Options
|
|
102
|
-
globalRateLimiter?:
|
|
103
|
+
globalRateLimiter?: HttpRateLimiter<HandlerContext>
|
|
103
104
|
sharedRateLimiters?: Map<string, RateLimiterI<HandlerContext>>
|
|
104
105
|
|
|
105
106
|
constructor(lexicons?: LexiconDoc[], opts: Options = {}) {
|
|
@@ -118,7 +119,7 @@ export class Server {
|
|
|
118
119
|
const { global, shared, creator, bypass } = opts.rateLimits
|
|
119
120
|
|
|
120
121
|
if (global) {
|
|
121
|
-
this.globalRateLimiter =
|
|
122
|
+
this.globalRateLimiter = HttpRateLimiter.from(
|
|
122
123
|
global.map((options) => creator(buildRateLimiterOptions(options))),
|
|
123
124
|
{ bypass },
|
|
124
125
|
)
|
|
@@ -426,7 +427,7 @@ export class Server {
|
|
|
426
427
|
authVerifier: AuthVerifierInternal<MethodAuthContext<P>, A> | null,
|
|
427
428
|
paramsVerifier: ParamsVerifierInternal<P>,
|
|
428
429
|
inputVerifier: InputVerifierInternal<I>,
|
|
429
|
-
routeLimiter:
|
|
430
|
+
routeLimiter: HttpRateLimiter<HandlerContext<A, P, I>> | undefined,
|
|
430
431
|
handler: MethodHandler<A, P, I, O>,
|
|
431
432
|
validateResOutput: null | OutputVerifierInternal<O>,
|
|
432
433
|
): RequestHandler {
|
|
@@ -664,14 +665,15 @@ export class Server {
|
|
|
664
665
|
>(
|
|
665
666
|
nsid: string,
|
|
666
667
|
config: MethodConfig<A, P, I, O>,
|
|
667
|
-
):
|
|
668
|
+
): HttpRateLimiter<HandlerContext<A, P, I>> | undefined {
|
|
668
669
|
// @NOTE global & shared rate limiters are instantiated with a context of
|
|
669
670
|
// HandlerContext which is compatible (more generic) with the context of
|
|
670
|
-
// this route specific rate limiters (C). For this reason, it's safe to
|
|
671
|
-
//
|
|
671
|
+
// this route specific rate limiters (C). For this reason, it's safe to cast
|
|
672
|
+
// the context of the global rate limiter to the context of the route
|
|
673
|
+
// specific rate limiter (HandlerContext<A, P, I>).
|
|
672
674
|
|
|
673
675
|
const globalRateLimiter = this.globalRateLimiter as
|
|
674
|
-
|
|
|
676
|
+
| HttpRateLimiter<HandlerContext<A, P, I>>
|
|
675
677
|
| undefined
|
|
676
678
|
|
|
677
679
|
// No route specific rate limiting configured, use the global rate limiter.
|
|
@@ -687,13 +689,18 @@ export class Server {
|
|
|
687
689
|
|
|
688
690
|
const rateLimiters = asArray(config.rateLimit).map((options, i) => {
|
|
689
691
|
if (isSharedRateLimitOpts(options)) {
|
|
690
|
-
const rateLimiter = this.sharedRateLimiters?.get(options.name)
|
|
692
|
+
const rateLimiter = this.sharedRateLimiters?.get(options.name) as
|
|
693
|
+
| RateLimiterI<HandlerContext<A, P, I>>
|
|
694
|
+
| undefined
|
|
691
695
|
|
|
692
696
|
// The route config references a shared rate limiter that does not
|
|
693
697
|
// exist. This is a configuration error.
|
|
694
698
|
assert(rateLimiter, `Shared rate limiter "${options.name}" not defined`)
|
|
695
699
|
|
|
696
|
-
return WrappedRateLimiter.from<
|
|
700
|
+
return WrappedRateLimiter.from<HandlerContext<A, P, I>>(
|
|
701
|
+
rateLimiter,
|
|
702
|
+
options,
|
|
703
|
+
)
|
|
697
704
|
} else {
|
|
698
705
|
return creator({
|
|
699
706
|
...options,
|
|
@@ -711,7 +718,9 @@ export class Server {
|
|
|
711
718
|
// the route specific rate limiters.
|
|
712
719
|
if (globalRateLimiter) rateLimiters.push(globalRateLimiter)
|
|
713
720
|
|
|
714
|
-
return
|
|
721
|
+
return HttpRateLimiter.from<HandlerContext<A, P, I>>(rateLimiters, {
|
|
722
|
+
bypass,
|
|
723
|
+
})
|
|
715
724
|
}
|
|
716
725
|
}
|
|
717
726
|
|
|
@@ -792,9 +801,20 @@ function buildRateLimiterOptions<C extends HandlerContext = HandlerContext>({
|
|
|
792
801
|
name,
|
|
793
802
|
calcKey = defaultKey,
|
|
794
803
|
calcPoints = defaultPoints,
|
|
795
|
-
|
|
804
|
+
failClosed = false,
|
|
805
|
+
durationMs,
|
|
806
|
+
points,
|
|
796
807
|
}: ServerRateLimitDescription<C>): RateLimiterOptions<C> {
|
|
797
|
-
return {
|
|
808
|
+
return {
|
|
809
|
+
durationMs,
|
|
810
|
+
points,
|
|
811
|
+
calcKey,
|
|
812
|
+
calcPoints,
|
|
813
|
+
keyPrefix: `rl-${name}`,
|
|
814
|
+
onError: failClosed
|
|
815
|
+
? undefined // Let the error propagate
|
|
816
|
+
: rateLimiterLoggerErrorHandler,
|
|
817
|
+
}
|
|
798
818
|
}
|
|
799
819
|
|
|
800
820
|
const defaultPoints: CalcPointsFn = () => 1
|
|
@@ -806,3 +826,19 @@ const defaultPoints: CalcPointsFn = () => 1
|
|
|
806
826
|
* @see {@link https://expressjs.com/en/guide/behind-proxies.html}
|
|
807
827
|
*/
|
|
808
828
|
const defaultKey: CalcKeyFn<HandlerContext> = ({ req }) => req.ip
|
|
829
|
+
|
|
830
|
+
async function rateLimiterLoggerErrorHandler(
|
|
831
|
+
err: unknown,
|
|
832
|
+
ctx: HandlerContext,
|
|
833
|
+
{ limiter: { keyPrefix, points, duration } }: RateLimiterErrorHandlerDetails,
|
|
834
|
+
): Promise<null> {
|
|
835
|
+
const { req } = ctx
|
|
836
|
+
const logger = isPinoHttpRequest(req) ? req.log : log
|
|
837
|
+
|
|
838
|
+
logger.error(
|
|
839
|
+
{ err, keyPrefix, points, duration },
|
|
840
|
+
'rate limiter failed to consume points',
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return null
|
|
844
|
+
}
|
package/tsconfig.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts"]
|
|
1
|
+
{"version":"7.0.0-dev.20260614.1","root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter-http.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts"]}
|
package/tsconfig.json
CHANGED