express-rate-limit / rate-limit-redis

A rate limiting store for express-rate-limit with Redis/Redict/Valkey/etc.
https://www.npmjs.com/package/rate-limit-redis
MIT License
184 stars 33 forks source link

Unable to create rate limiter with redis store (io-redis) #200

Closed pharapeti closed 10 months ago

pharapeti commented 10 months ago

Description

I have been able to use the rate limiter just fine with the memory store, however when I try to use the redis store (io-redis), I get the following error.

/usr/app/server/backend/src/config/rateLimiter.ts:40
  store: new RedisStore( {
         ^
TypeError: rate_limit_redis_1.default is not a constructor
    at Object.<anonymous> (/usr/app/server/backend/src/config/rateLimiter.ts:40:10)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module.m._compile (/usr/app/server/backend/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Object.require.extensions.<computed> [as .ts] (/usr/app/server/backend/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Function.Module._load (node:internal/modules/cjs/loader:960:12)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:110:18)
    at Object.<anonymous> (/usr/app/server/backend/src/app.ts:25:1)

Here is what I'm trying to do:

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import RedisClient from 'ioredis';

// Create a `ioredis` client
const client = new RedisClient( {
  host: <connection string>,
  port: <number>,
  db: <number>
} );

export const rateLimiter = rateLimit( {
  windowMs: 10 * 1000, // 10 seconds
  limit: 100, // Limit each IP to 100 requests per `window` (here, per 10 seconds).
  standardHeaders: 'draft-7', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers.

  // Redis store configuration
  store: new RedisStore( {
    // @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
    sendCommand: async ( ...args: string[] ): unknown => client.call( ...args ),
  } ),
} );

Library version

7.1.2

Node version

18.16.1

Typescript version (if you are using it)

4.9.5

Module system

ESM

gamemaker1 commented 10 months ago

Hi @pharapeti,

Could you please change the import RedisStore from ... to import { RedisStore } from ...?

pharapeti commented 10 months ago

Hi @pharapeti,

Could you please change the import RedisStore from ... to import { RedisStore } from ...?

Doing so results in the generation of a typescript error:

Module '"rate-limit-redis"' has no exported member 'RedisStore'. Did you mean to use 'import RedisStore from "rate-limit-redis"' instead?ts(2614)

And results in the same type error

TypeError: rate_limit_redis_1.RedisStore is not a constructor
    at Object.<anonymous> (/usr/app/server/backend/src/config/rateLimiter.ts:40:10)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module.m._compile (/usr/app/server/backend/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Object.require.extensions.<computed> [as .ts] (/usr/app/server/backend/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1119:32)
    at Function.Module._load (node:internal/modules/cjs/loader:960:12)
    at Module.require (node:internal/modules/cjs/loader:1143:19)
    at require (node:internal/modules/cjs/helpers:110:18)
    at Object.<anonymous> (/usr/app/server/backend/src/app.ts:25:1)
nfriedly commented 10 months ago

Just to double-check, what version of rate-limit-redis are you on? (npm ls rate-limit-redis)

pharapeti commented 10 months ago

Just to double-check, what version of rate-limit-redis are you on? (npm ls rate-limit-redis)

I'm on these version right now - but I can change them if necessary

express-rate-limit 7.1.2 rate-limit redis: 4.1.2 ioredis: 5.3.2

gamemaker1 commented 10 months ago

Could you please share your tsconfig.json?

pharapeti commented 10 months ago

Could you please share your tsconfig.json?

Sure!

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": [
      "dom",
      "ES2020",
      "esnext.asynciterable"
    ],
    "typeRoots": [
      "./node_modules/@types",
      "./src/types",
      "./test/types"
    ],
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "module": "commonjs",
    "pretty": true,
    "resolveJsonModule": true,
    "sourceMap": true,
    "baseUrl": ".",
    "outDir": "./build",
    "allowJs": true,
    "checkJs": true,
    "noEmit": false
  },
  "include": [
    "./src/**/*",
    "./test/**/*.?s",
    "./migrations/*.ts",
    "jest.config.js",
    ".eslintrc.js"
  ],
  "exclude": [
    "node_modules"
  ],
  "ts-node": {
    "transpileOnly": true
  }
}
gamemaker1 commented 10 months ago

Hmm I don't see anything wrong.. could you try putting console.dir(RedisStore, {depth: undefined}) just after import RedisStore from 'rate-limit-redis' and paste the output here?

Thanks

pharapeti commented 10 months ago

Hmm I don't see anything wrong.. could you try putting console.dir(RedisStore, {depth: undefined}) just after import RedisStore from 'rate-limit-redis' and paste the output here?

Thanks

The following code:

console.log( '\n' );
console.log( 'express-rate-limit' );
console.dir( rateLimit, { depth: undefined } );
console.log( '\niosredis' );
console.dir( RedisClient, { depth: undefined } );
console.log( '\nrate-limit-redis' );
console.dir( RedisStore, { depth: undefined } );
console.log( '\n' );

Returns:

express-rate-limit
<ref *1> [Function: rateLimit] {
  default: [Circular *1],
  rateLimit: [Circular *1],
  MemoryStore: [class MemoryStore]
}

iosredis
[class Redis extends Commander] {
  Cluster: [Getter],
  Command: [Getter],
  defaultOptions: {
    port: 6379,
    host: 'localhost',
    family: 4,
    connectTimeout: 10000,
    disconnectTimeout: 2000,
    retryStrategy: [Function: retryStrategy],
    keepAlive: 0,
    noDelay: true,
    connectionName: null,
    sentinels: null,
    name: null,
    role: 'master',
    sentinelRetryStrategy: [Function: sentinelRetryStrategy],
    sentinelReconnectStrategy: [Function: sentinelReconnectStrategy],
    natMap: null,
    enableTLSForSentinelMode: false,
    updateSentinels: true,
    failoverDetector: false,
    username: null,
    password: null,
    db: 0,
    enableOfflineQueue: true,
    enableReadyCheck: true,
    autoResubscribe: true,
    autoResendUnfulfilledCommands: true,
    lazyConnect: false,
    keyPrefix: '',
    reconnectOnError: null,
    readOnly: false,
    stringNumbers: false,
    maxRetriesPerRequest: 20,
    maxLoadingRetryTime: 10000,
    enableAutoPipelining: false,
    autoPipeliningIgnoredCommands: [],
    sentinelMaxConnections: 10
  },
  default: [Getter],
  Redis: [Getter],
  ScanStream: [Getter],
  Pipeline: [Getter],
  AbstractConnector: [Getter],
  SentinelConnector: [Getter],
  SentinelIterator: [Getter],
  ReplyError: [class ReplyError extends RedisError],
  print: [Function: print]
}

rate-limit-redis
undefined
gamemaker1 commented 10 months ago

Sorry for the late reply; I think I finally figured out why this error is happening.

Does your package.json have type: module or type: commonjs, or no type field set at all?

If it's set to type: module, changing the tsconfig compilerOptions.module to anything besides commonjs, like es2022 or esnext or node16 makes the error go away.

If it's not set, or set to commonjs, this error occurs. You can make it go away by setting compilerOptions.esModuleInterop to true.


The reason the error is occurring is that the library supports importing the default export in commonjs and esm, but typescript thinks that it's only supported in esm and thus appends .default everywhere RedisStore is used in the generated commonjs code. Setting esModuleInterop to true lets typescript know that it doesn't need to do that.

pharapeti commented 10 months ago

Without making any of the changes @gamemaker1 suggested (I'm still using default export), I upgraded to version v4.2.0 and now I am able to use this package successfully.

Thank you for the support @gamemaker1 and @nfriedly :rocket:

nfriedly commented 10 months ago

Well, I'm not sure why that fixed it, but I'm glad it's working for you now :)