Skip to main content
Glama
rateLimiter.ts6.8 kB
import rateLimit from 'express-rate-limit'; import { Request, Response, NextFunction } from 'express'; import Redis from 'redis'; // Redis client for distributed rate limiting let redisClient: Redis.RedisClientType | null = null; // Initialize Redis client (optional - falls back to memory store) export const initializeRedis = async (redisUrl?: string): Promise<void> => { try { redisClient = Redis.createClient({ url: redisUrl || process.env.REDIS_URL || 'redis://localhost:6379' }); redisClient.on('error', (err) => { console.warn('Redis rate limiter error:', err); // Don't throw - fallback to memory store }); await redisClient.connect(); console.log('Redis rate limiter initialized'); } catch (error) { console.warn('Redis initialization failed, using memory store for rate limiting:', error); redisClient = null; } }; // Note: Redis store implementation removed for compatibility with express-rate-limit v7 // Memory store is used by default, which is suitable for single-instance deployments // For distributed deployments, consider using a compatible Redis store package // Rate limiting configurations for different endpoint types export const rateLimitConfigs = { // Very strict for sensitive operations strict: { windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // limit each IP to 10 requests per windowMs message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }, // Standard for most API endpoints standard: { windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }, // Lenient for read-only operations lenient: { windowMs: 15 * 60 * 1000, // 15 minutes max: 1000, // limit each IP to 1000 requests per windowMs message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false } }; // Create rate limiter with optional Redis store export const createRateLimiter = (config: typeof rateLimitConfigs.standard, keyPrefix = 'rl') => { const limiterConfig = { ...config, keyGenerator: (req: Request) => `${keyPrefix}:${req.ip}:${req.path}`, // Note: Redis store implementation removed for compatibility with express-rate-limit v7 // Falls back to memory store, which is suitable for single-instance deployments skip: (req: Request) => { // Skip rate limiting for health checks and internal endpoints return req.path === '/health' || req.path === '/metrics'; } }; return rateLimit(limiterConfig); }; // Pre-configured rate limiters export const strictRateLimiter = createRateLimiter(rateLimitConfigs.strict, 'strict'); export const standardRateLimiter = createRateLimiter(rateLimitConfigs.standard, 'standard'); export const lenientRateLimiter = createRateLimiter(rateLimitConfigs.lenient, 'lenient'); // Tally API rate limiting - token bucket implementation class TallyApiRateLimiter { private tokens: number; private lastRefill: number; private readonly maxTokens: number; private readonly refillRate: number; // tokens per second constructor(maxTokens = 100, refillRate = 1) { this.maxTokens = maxTokens; this.refillRate = refillRate; this.tokens = maxTokens; this.lastRefill = Date.now(); } private refillTokens(): void { const now = Date.now(); const timePassed = (now - this.lastRefill) / 1000; // convert to seconds const tokensToAdd = Math.floor(timePassed * this.refillRate); if (tokensToAdd > 0) { this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } } canMakeRequest(): boolean { this.refillTokens(); return this.tokens > 0; } consumeToken(): boolean { this.refillTokens(); if (this.tokens > 0) { this.tokens--; return true; } return false; } getTokensRemaining(): number { this.refillTokens(); return this.tokens; } getTimeUntilNextToken(): number { if (this.tokens > 0) return 0; return Math.ceil(1000 / this.refillRate); // milliseconds until next token } } // Global Tally API rate limiter instance export const tallyApiLimiter = new TallyApiRateLimiter(100, 1); // 100 tokens, 1 per second // Middleware to check Tally API rate limit before making requests export const tallyApiRateLimit = (_req: Request, res: Response, next: NextFunction): void => { if (!tallyApiLimiter.canMakeRequest()) { const waitTime = tallyApiLimiter.getTimeUntilNextToken(); res.status(429).json({ error: 'Tally API rate limit exceeded', message: 'Too many requests to Tally API, please try again later', retryAfter: Math.ceil(waitTime / 1000), tokensRemaining: tallyApiLimiter.getTokensRemaining() }); return; } // Consume token and proceed tallyApiLimiter.consumeToken(); // Add rate limit info to response headers res.set({ 'X-Tally-RateLimit-Limit': '100', 'X-Tally-RateLimit-Remaining': tallyApiLimiter.getTokensRemaining().toString(), 'X-Tally-RateLimit-Reset': new Date(Date.now() + tallyApiLimiter.getTimeUntilNextToken()).toISOString() }); next(); }; // Composite middleware that applies both our rate limiting and Tally API limiting export const createCompositeRateLimiter = (config: typeof rateLimitConfigs.standard = rateLimitConfigs.standard) => { const ourLimiter = createRateLimiter(config); return [ourLimiter, tallyApiRateLimit]; }; // Enhanced error handler for rate limiting export const rateLimitErrorHandler = (err: any, req: Request, res: Response, next: NextFunction): void => { if (err && err.status === 429) { res.status(429).json({ error: 'Rate limit exceeded', message: err.message || 'Too many requests, please try again later', retryAfter: err.retryAfter || 900, // 15 minutes default timestamp: new Date().toISOString(), path: req.path, ip: req.ip }); return; } next(err); }; // Utility to get current rate limit status export const getRateLimitStatus = (req: Request): object => { return { ip: req.ip, path: req.path, tallyApiTokensRemaining: tallyApiLimiter.getTokensRemaining(), tallyApiTimeUntilNextToken: tallyApiLimiter.getTimeUntilNextToken(), headers: { rateLimit: req.get('X-RateLimit-Limit'), rateLimitRemaining: req.get('X-RateLimit-Remaining'), rateLimitReset: req.get('X-RateLimit-Reset') } }; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server