{"record":{"id":"879127abfe9c7a99","repo":"calcom/cal.diy","slug":"customthrottlerguard-invalid-api-key","errorCode":null,"errorMessage":"CustomThrottlerGuard - Invalid API Key","messagePattern":"CustomThrottlerGuard - Invalid API Key","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"apps/api/v2/src/lib/throttler-guard.ts","lineNumber":160,"sourceCode":"    const cacheKey = `rate_limit:${tracker}`;\n\n    const cachedRateLimits = await this.storageService.redis.get(cacheKey);\n    if (cachedRateLimits) {\n      /*this.logger.verbose(`Tracker \"${tracker}\" rate limits retrieved from redis cache:\n        ${cachedRateLimits}\n      `);*/\n      return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));\n    }\n\n    const apiKey = tracker.replace(\"api_key_\", \"\");\n    let rateLimits: RateLimitType[];\n    const apiKeyRecord = await this.dbRead.prisma.apiKey.findUnique({\n      where: { hashedKey: apiKey },\n      select: { id: true },\n    });\n\n    if (!apiKeyRecord) {\n      throw new UnauthorizedException(\"CustomThrottlerGuard - Invalid API Key\");\n    }\n\n    rateLimits = await this.dbRead.prisma.rateLimit.findMany({\n      where: { apiKeyId: apiKeyRecord.id },\n      select: { name: true, limit: true, ttl: true, blockDuration: true },\n    });\n\n    if (!rateLimits || rateLimits.length === 0) {\n      rateLimits = [this.getDefaultRateLimit(tracker)];\n      /*this.logger.verbose(`Tracker \"${tracker}\" rate limits not found in database. Using default rate limits:\n        ${JSON.stringify(rateLimits, null, 2)}`);*/\n    }\n\n    await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), \"EX\", 3600);\n\n    return rateLimits;\n  }\n","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/lib/throttler-guard.ts#L142-L178","documentation":"Thrown by CustomThrottlerGuard.getRateLimitsForApiKeyTracker when the tracker has an 'api_key_' prefix (meaning the Authorization header started with the API key prefix, default 'cal_') but the SHA-256 hash of the stripped key does not match any record in the apiKey table. The guard recognizes the key format but cannot find it in the database to load custom rate limits. This throws UnauthorizedException (HTTP 401) rather than ThrottlerException because it is an authentication failure, not a rate-limit violation.","triggerScenarios":"A request sends 'Authorization: Bearer cal_<key>' where the key was deleted from the database, expired, or belongs to a different environment. The API_KEY_PREFIX env differs between the client and server (e.g. client uses 'cal_' but server expects a custom prefix), causing stripApiKey to produce the wrong hash. The key was rotated/refreshed (old key is deleted) and the client still uses the stale key.","commonSituations":"Stale API key after a refresh operation (refreshApiKey deletes the old key at line 76). Mismatched API_KEY_PREFIX between environments (dev vs staging vs production). Key copied with extra whitespace or truncation. Database read replica lag causing a just-created key to be temporarily invisible.","solutions":["Verify the API key is complete, correctly formatted, and starts with the expected prefix (check API_KEY_PREFIX env on the server).","Confirm the key still exists in the database and has not been deleted or expired.","If the key was recently refreshed via the refresh endpoint, update the client to use the new key returned in the refresh response.","Check for database replication lag if the key was just created: wait a few seconds and retry."],"exampleFix":"// before: hardcoded key that may be stale\nconst apiKey = 'cal_stale_key_from_config';\n\n// after: validate key existence and handle 401 gracefully\nconst callApi = async (apiKey: string) => {\n  const res = await fetch('/v2/event-types', {\n    headers: { Authorization: `Bearer ${apiKey}` }\n  });\n  if (res.status === 401) {\n    throw new Error('API key invalid or expired. Refresh the key and retry.');\n  }\n  return res.json();\n};","handlingStrategy":"validation","validationCode":"// Validate API key format and prefix before sending\nconst API_KEY_PREFIX = 'cal_'; // match server's API_KEY_PREFIX env\nconst isValidApiKeyFormat = (key: string): boolean => {\n  if (!key || typeof key !== 'string') return false;\n  if (!key.startsWith(API_KEY_PREFIX)) return false;\n  const stripped = key.slice(API_KEY_PREFIX.length);\n  return stripped.length > 10; // minimum reasonable length\n};\nif (!isValidApiKeyFormat(apiKey)) {\n  throw new Error(`Invalid API key format. Expected prefix: ${API_KEY_PREFIX}`);\n}","typeGuard":"const isValidApiKey = (key: unknown): key is string =>\n  typeof key === 'string' && key.startsWith('cal_') && key.length > 10;","tryCatchPattern":"// Handle 401 from throttler by refreshing the key\ntry {\n  await apiClient.get('/v2/event-types');\n} catch (err: any) {\n  if (err?.response?.status === 401 && err?.response?.data?.message?.includes('Invalid API Key')) {\n    // Key is stale or invalid; generate a new one\n    const newKey = await generateNewApiKey();\n    apiClient.setHeader('Authorization', `Bearer ${newKey}`);\n    // Retry once with the new key\n    return apiClient.get('/v2/event-types');\n  }\n  throw err;\n}","preventionTips":["Store API keys in a secure secret manager (not in code or config files checked into git).","After refreshing a key, immediately update all clients to use the new key — the old key is deleted instantly.","Verify the API_KEY_PREFIX matches between client and server environments before deployment.","Log key creation and deletion timestamps to diagnose stale-key issues."],"tags":["authentication","api-key","nestjs","api-v2","unauthorized"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}