{"id":"7199dbd01cffff52","repo":"redis/node-redis","slug":"expirationrefreshratio-must-be-greater-or-equal-to","errorCode":null,"errorMessage":"expirationRefreshRatio must be greater or equal to 0","messagePattern":"expirationRefreshRatio must be greater or equal to 0","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/authx/token-manager.ts","lineNumber":149,"sourceCode":" *\n * The TokenManager should be disposed when it is no longer needed by calling the dispose method on the Disposable\n * returned by start.\n */\nexport class TokenManager<T> {\n  private currentToken: Token<T> | null = null;\n  private refreshTimeout: NodeJS.Timeout | null = null;\n  private listener: TokenStreamListener<T> | null = null;\n  private retryAttempt: number = 0;\n\n  constructor(\n    private readonly identityProvider: IdentityProvider<T>,\n    private readonly config: TokenManagerConfig\n  ) {\n    if (this.config.expirationRefreshRatio > 1) {\n      throw new Error('expirationRefreshRatio must be less than or equal to 1');\n    }\n    if (this.config.expirationRefreshRatio < 0) {\n      throw new Error('expirationRefreshRatio must be greater or equal to 0');\n    }\n  }\n\n  /**\n   * Starts the token manager and returns a Disposable that can be used to stop the token manager.\n   *\n   * @param listener The listener that will receive token updates.\n   * @param initialDelayMs The initial delay in milliseconds before the first token refresh.\n   */\n  public start(listener: TokenStreamListener<T>, initialDelayMs: number = 0): Disposable {\n    if (this.listener) {\n      this.stop();\n    }\n\n    this.listener = listener;\n    this.retryAttempt = 0;\n\n    this.scheduleNextRefresh(initialDelayMs);","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/authx/token-manager.ts#L131-L167","documentation":"Companion guard to error [3]: the TokenManager constructor rejects `expirationRefreshRatio < 0`. A negative ratio would compute a negative refresh delay, breaking the scheduling math (Math.floor of a negative ttlMs fraction) and effectively never refreshing correctly. Both bounds checks fire in the constructor, so the TokenManager never enters an inconsistent scheduling state.","triggerScenarios":"Passing `expirationRefreshRatio: -1` or any negative number; an arithmetic slip (subtracting instead of adding, or 0 - value) producing a negative config; reading an unset numeric env var parsed as NaN (note: NaN comparisons are false, so NaN slips past, but a parsed negative literal triggers this).","commonSituations":"Default/uninitialized config expressed as -1 'sentinel'; sign error in config transformation; env var typo producing a leading minus.","solutions":["Set expirationRefreshRatio to a value in [0,1] (e.g. 0.7).","Validate config at load time and reject non-finite or out-of-range values before constructing the TokenManager.","If using a sentinel default, coerce it to a valid ratio (e.g. 0.7) when unset."],"exampleFix":"// before\nnew TokenManager(provider, { expirationRefreshRatio: -1 });\n\n// after\nnew TokenManager(provider, { expirationRefreshRatio: 0.7 });","handlingStrategy":"validation","validationCode":"if (!Number.isFinite(cfg.expirationRefreshRatio) || cfg.expirationRefreshRatio < 0) {\n  throw new RangeError('expirationRefreshRatio must be a finite number >= 0');\n}","typeGuard":"function isNonNegativeRatio(v: unknown): v is number {\n  return typeof v === 'number' && Number.isFinite(v) && v >= 0;\n}","tryCatchPattern":null,"preventionTips":["Reject sentinel defaults like -1 at config load; coerce to a valid ratio.","Add a schema check for any config sourced from env/remote."],"tags":["config","validation","authx","token-manager"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}