@@ -13,9 +13,14 @@ const RATE_LIMIT = {
1313 max : 100 , // limit each IP to 100 requests per windowMs
1414} ;
1515
16+ // Memory cache for frequent requests to reduce Redis calls
17+ // This cache is shared across all requests to the same server instance
18+ const memCache = new Map < string , { count : number ; expires : number } > ( ) ;
19+
1620/**
1721 * Rate limiting middleware for public endpoints
1822 * Uses Redis to track request counts across multiple instances
23+ * Optimized to use Redis pipeline for better performance
1924 */
2025export async function rateLimiter (
2126 c : Context ,
@@ -33,17 +38,55 @@ export async function rateLimiter(
3338 const key = `ratelimit:${ ip } ` ;
3439
3540 try {
36- // Get current request count
37- const requests = await redis . incr ( key ) ;
38-
39- // Set expiry on first request
40- if ( requests === 1 ) {
41- await redis . expire ( key , RATE_LIMIT . windowMs / 1000 ) ;
41+ let requests : number ;
42+ let ttl : number ;
43+
44+ // Check memory cache first to avoid Redis calls for frequent requests
45+ const now = Date . now ( ) ;
46+ const cached = memCache . get ( key ) ;
47+
48+ if ( cached && cached . expires > now ) {
49+ // Use cached values if they haven't expired
50+ requests = cached . count + 1 ;
51+ ttl = Math . floor ( ( cached . expires - now ) / 1000 ) ;
52+
53+ // Update the cache with incremented count
54+ memCache . set ( key , {
55+ count : requests ,
56+ expires : cached . expires ,
57+ } ) ;
58+ } else {
59+ // Cache miss or expired, use Redis pipeline to batch commands for better performance
60+ // This reduces network roundtrips to Redis
61+ const pipeline = redis . pipeline ( ) ;
62+ pipeline . incr ( key ) ;
63+ pipeline . ttl ( key ) ;
64+
65+ const results = await pipeline . exec ( ) ;
66+
67+ if ( ! results || results . length < 2 ) {
68+ // If pipeline fails, allow the request to proceed
69+ console . error ( "Rate limiting pipeline error: Invalid results" ) ;
70+ await next ( ) ;
71+ return undefined ;
72+ }
73+
74+ requests = results [ 0 ] as number ;
75+ ttl = results [ 1 ] as number ;
76+
77+ // Set expiry on first request
78+ if ( requests === 1 || ttl < 0 ) {
79+ await redis . expire ( key , RATE_LIMIT . windowMs / 1000 ) ;
80+ ttl = RATE_LIMIT . windowMs / 1000 ;
81+ }
82+
83+ // Update memory cache with values from Redis
84+ memCache . set ( key , {
85+ count : requests ,
86+ expires : now + ttl * 1000 ,
87+ } ) ;
4288 }
4389
44- // Get TTL for headers
45- const ttl = await redis . ttl ( key ) ;
46-
4790 // Set rate limit headers
4891 c . header ( "X-RateLimit-Limit" , RATE_LIMIT . max . toString ( ) ) ;
4992 c . header (
@@ -74,8 +117,8 @@ export async function rateLimiter(
74117}
75118
76119/**
77- * Security headers middleware
78- * Adds essential security headers to responses
120+ * Security and caching headers middleware
121+ * Adds essential security and performance-related headers to responses
79122 */
80123export async function securityHeaders (
81124 c : Context ,
@@ -90,30 +133,64 @@ export async function securityHeaders(
90133 // Enforce HTTPS for secure feed access
91134 c . header ( "Strict-Transport-Security" , "max-age=31536000; includeSubDomains" ) ;
92135
136+ // Add performance-related headers for GET requests to feed endpoints
137+ const path = c . req . path ;
138+ if (
139+ c . req . method === "GET" &&
140+ ( path . endsWith ( ".xml" ) || path . endsWith ( ".json" ) || path === "/" )
141+ ) {
142+ // Only add Cache-Control if not already set by the route handler
143+ if ( ! c . res . headers . has ( "Cache-Control" ) ) {
144+ // Public feeds can be cached by browsers and proxies
145+ c . header ( "Cache-Control" , "public, max-age=600" ) ; // 10 minutes
146+ }
147+
148+ // Add Vary header to ensure proper caching based on these request headers
149+ c . header ( "Vary" , "Accept, Accept-Encoding" ) ;
150+
151+ // Add a default ETag if not already set
152+ if ( ! c . res . headers . has ( "ETag" ) ) {
153+ // Generate a simple ETag based on the current time (not ideal but better than nothing)
154+ // In production, this should be based on content hash
155+ c . header ( "ETag" , `"${ Date . now ( ) . toString ( 36 ) } "` ) ;
156+ }
157+ }
158+
93159 return c . res ;
94160}
95161
96162/**
97163 * Request timeout middleware
98164 * Adds timeout protection for long-running requests
165+ * Optimized to avoid unnecessary Promise overhead
99166 */
100167export async function requestTimeout (
101168 c : Context ,
102169 next : Next ,
103170) : Promise < Response | undefined > {
104171 const TIMEOUT = 30000 ; // 30 seconds
105172
106- const timeoutPromise = new Promise ( ( _ , reject ) => {
107- setTimeout ( ( ) => {
108- reject ( new Error ( "Request timeout" ) ) ;
109- } , TIMEOUT ) ;
110- } ) ;
173+ // Use AbortController for more efficient timeout handling
174+ const controller = new AbortController ( ) ;
175+ const timeoutId = setTimeout ( ( ) => {
176+ controller . abort ( ) ;
177+ } , TIMEOUT ) ;
111178
112179 try {
113- await Promise . race ( [ next ( ) , timeoutPromise ] ) ;
180+ // Store the signal in the context for downstream middleware
181+ c . set ( "abortSignal" , controller . signal ) ;
182+
183+ // Execute the next middleware with timeout
184+ await next ( ) ;
185+
186+ // Clear the timeout if the request completes successfully
187+ clearTimeout ( timeoutId ) ;
188+
114189 return c . res ;
115190 } catch ( error ) {
116- if ( error instanceof Error && error . message === "Request timeout" ) {
191+ clearTimeout ( timeoutId ) ;
192+
193+ if ( error instanceof Error && error . name === "AbortError" ) {
117194 return c . json (
118195 {
119196 error : "Request Timeout" ,
@@ -122,6 +199,7 @@ export async function requestTimeout(
122199 408 ,
123200 ) ;
124201 }
202+
125203 throw error ;
126204 }
127205}
0 commit comments