Skip to content

Commit 23bcc0b

Browse files
JamesMGreeneheiskrrachmari
authored
Use node-redis for rate limiter (github#18416)
* Use [node-]redis as a direct dependency * Extract Redis client creation to its own module * Attach extensive logging in the Redis client creation module * Allow the rate limiter to pass requests when Redis is disconnected * Update rate-limit-redis * Default error input to empty object for formatRedisError method * Provide a name for the rate limiter's Redis client Co-authored-by: Kevin Heis <[email protected]> Co-authored-by: Rachael Sewell <[email protected]>
1 parent 460cb54 commit 23bcc0b

File tree

4 files changed

+92
-17
lines changed

4 files changed

+92
-17
lines changed

lib/redis/create-client.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const Redis = require('redis')
2+
3+
const { REDIS_MIN_DB, REDIS_MAX_DB } = process.env
4+
5+
// By default, every Redis instance supports database numbers 0 - 15
6+
const redisMinDb = REDIS_MIN_DB || 0
7+
const redisMaxDb = REDIS_MAX_DB || 15
8+
9+
function formatRedisError (error = {}) {
10+
const { code } = error
11+
const preamble = error.constructor.name + (code ? ` with code "${code}"` : '')
12+
return preamble + error.toString()
13+
}
14+
15+
module.exports = function createClient (options = {}) {
16+
const { db, name, url } = options
17+
18+
// If no Redis URL is provided, bail out
19+
// NOTE: Could support other options like `host`, `port`, and `path` but
20+
// choosing not to for the time being!
21+
if (!url) return null
22+
23+
// Verify database number is within range
24+
if (db != null) {
25+
if (!Number.isInteger(db) || db < redisMinDb || db > redisMaxDb) {
26+
throw new TypeError(
27+
`Redis database number must be an integer between ${redisMinDb} and ${redisMaxDb} but was: ${JSON.stringify(db)}`
28+
)
29+
}
30+
}
31+
32+
// Create the client
33+
const client = Redis.createClient(url, {
34+
// Only add this configuration for TLS-enabled Redis URL values.
35+
// Otherwise, it breaks for local Redis instances without TLS enabled.
36+
...url.startsWith('rediss://') && {
37+
tls: {
38+
// Required for production Heroku Redis
39+
rejectUnauthorized: false
40+
}
41+
},
42+
43+
// Expand whatever other options and overrides were provided
44+
...options
45+
})
46+
47+
// If a `name` was provided, use it in the prefix for logging event messages
48+
const logPrefix = '[redis' + (name ? ` (${name})` : '') + '] '
49+
50+
// Add event listeners for basic logging
51+
client.on('connect', () => { console.log(logPrefix, 'Connection opened') })
52+
client.on('ready', () => { console.log(logPrefix, 'Ready to receive commands') })
53+
client.on(
54+
'reconnecting',
55+
({
56+
attempt,
57+
delay,
58+
// The rest are unofficial properties but currently supported
59+
error,
60+
total_retry_time: totalRetryTime,
61+
times_connected: timesConnected
62+
}) => {
63+
console.log(
64+
logPrefix,
65+
'Reconnecting,',
66+
`attempt ${attempt}`,
67+
`with ${delay} delay`,
68+
`due to ${formatRedisError(error)}.`,
69+
`Elapsed time: ${totalRetryTime}.`,
70+
`Successful connections: ${timesConnected}.`
71+
)
72+
}
73+
)
74+
client.on('end', () => { console.log(logPrefix, 'Connection closed') })
75+
client.on('warning', (msg) => { console.warn(logPrefix, 'Warning:', msg) })
76+
client.on('error', (error) => { console.error(logPrefix, formatRedisError(error)) })
77+
78+
return client
79+
}

middleware/rate-limit.js

+8-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const rateLimit = require('express-rate-limit')
22
const RedisStore = require('rate-limit-redis')
3-
const Redis = require('ioredis')
3+
const createRedisClient = require('../lib/redis/create-client')
44

55
const isProduction = process.env.NODE_ENV === 'production'
66
const { REDIS_URL } = process.env
@@ -15,21 +15,16 @@ module.exports = rateLimit({
1515
// Don't rate limit requests for 200s and redirects
1616
// Or anything with a status code less than 400
1717
skipSuccessfulRequests: true,
18-
// When available, use Redis
18+
// When available, use Redis; if not, defaults to an in-memory store
1919
store: REDIS_URL && new RedisStore({
20-
client: new Redis(REDIS_URL, {
20+
client: createRedisClient({
21+
url: REDIS_URL,
2122
db: rateLimitDatabaseNumber,
22-
23-
// Only add this configuration for TLS-enabled REDIS_URL values.
24-
// Otherwise, it breaks for local Redis instances without TLS enabled.
25-
...REDIS_URL.startsWith('rediss://') && {
26-
tls: {
27-
// Required for production Heroku Redis
28-
rejectUnauthorized: false
29-
}
30-
}
23+
name: 'rate-limit'
3124
}),
3225
// 1 minute (or practically unlimited outside of production)
33-
expiry: isProduction ? EXPIRES_IN_AS_SECONDS : 1 // Redis configuration in `s`
26+
expiry: isProduction ? EXPIRES_IN_AS_SECONDS : 1, // Redis configuration in `s`
27+
// If Redis is not connected, let the request succeed as failover
28+
passIfNotConnected: true
3429
})
3530
})

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@
7575
"node-fetch": "^2.6.1",
7676
"parse5": "^6.0.1",
7777
"port-used": "^2.0.8",
78-
"rate-limit-redis": "^2.0.0",
78+
"rate-limit-redis": "^2.1.0",
7979
"react": "^17.0.1",
8080
"react-dom": "^17.0.1",
81+
"redis": "^3.0.2",
8182
"rehype-autolink-headings": "^2.0.5",
8283
"rehype-highlight": "^3.1.0",
8384
"rehype-raw": "^4.0.2",

0 commit comments

Comments
 (0)