UNPKG

dynamic-rate-limiter-middleware

Version:

Intelligent rate limiting middleware with MongoDB integration and caching for Node.js applications

172 lines (152 loc) 6.19 kB
const axios = require('axios'); const CacheManager = require('../utils/cache'); const HitScheduler = require('../utils/scheduler'); const Hit = require('../models/Hit'); const ApiConfig = require('../models/ApiConfig'); const config = require('../config/config'); /** * Creates a rate limiter middleware instance * @param {Object} options Configuration options * @returns {Function} Express middleware function */ function createRateLimiter(options = {}) { // Initialize components const cache = new CacheManager(options); const scheduler = new HitScheduler(options); return async function rateLimiter(req, res, next) { const startTime = Date.now(); const endpoint = req.path; const method = req.method; let hit = null; // Declare hit outside try block try { // Get or create API configuration const apiConfig = await ApiConfig.findByEndpoint(endpoint, method) || await new ApiConfig({ endpoint, method, baseUrl: options.baseUrl, rateLimit: { daily: options.maxHitsPerDay || config.defaultOptions.maxHitsPerDay } }).save(); if (!apiConfig.isActive) { return res.status(403).json({ error: 'API endpoint is not active' }); } // Get or create hit record hit = new Hit({ userId: req.user ? req.user.id : null, endpoint, method, dailyLimit: apiConfig.rateLimit.daily }); await hit.save(); // Check cache first const cacheKey = CacheManager.generateKey(endpoint, req.query); if (apiConfig.cacheConfig.enabled) { const cachedResponse = cache.get(cacheKey); if (cachedResponse) { return res.json(cachedResponse); } } // Check rate limits and scheduling const schedulingDecision = scheduler.calculateAvailableHits( hit.hits, hit.lastHit ); if (!schedulingDecision.canMakeHit) { // Try to serve stale cache if available const cachedResponse = cache.get(cacheKey); if (cachedResponse) { return res.json({ data: cachedResponse, source: 'cache', warning: 'Rate limit reached, serving cached data' }); } return res.status(429).json({ error: 'Rate limit exceeded', waitTime: schedulingDecision.waitTime, reason: schedulingDecision.reason }); } // Prepare for external API call const axiosConfig = { method, url: `${apiConfig.baseUrl}${endpoint}`, headers: Object.fromEntries(apiConfig.headers), params: req.query, data: req.body }; // Make the external API call with retry logic let response; let retryCount = 0; while (retryCount <= apiConfig.retryConfig.attempts) { try { response = await axios(axiosConfig); break; } catch (error) { if (!apiConfig.shouldRetry(error.response?.status) || retryCount === apiConfig.retryConfig.attempts) { throw error; } retryCount++; const delay = scheduler.getRetryDelay(retryCount); await new Promise(resolve => setTimeout(resolve, delay)); } } // Record successful hit const responseTime = Date.now() - startTime; await hit.recordHit(responseTime, true); // Cache the response if (apiConfig.cacheConfig.enabled && response.data) { cache.set(cacheKey, response.data, apiConfig.cacheConfig.ttl); } // Send response return res.json({ data: response.data, source: 'api', metrics: { responseTime, remainingHits: apiConfig.rateLimit.daily - hit.hits } }); } catch (error) { // Record error only if hit exists if (hit) { try { await hit.recordError( error.response?.status || 500, error.message ); } catch (recordError) { // Silently handle error recording failure console.error('Failed to record error:', recordError); } } // Handle errors const status = error.response?.status || 500; const errorResponse = { error: error.message, status, endpoint, method }; // Try to use fallback response if configured try { const apiConfig = await ApiConfig.findByEndpoint(endpoint, method); if (apiConfig?.errorHandling.fallbackResponse) { return res.status(status).json({ ...apiConfig.errorHandling.fallbackResponse, source: 'fallback', originalError: errorResponse }); } } catch (configError) { // If we can't get the config, continue to default error handling console.error('Failed to get API config for fallback:', configError); } next(error); } }; } module.exports = createRateLimiter;