@@ -15,14 +15,47 @@ export default {
1515 async fetch ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > {
1616 const analytics = new PosthogEventCapture ( env ) ;
1717
18- // --- Rate limiting ---
19- const { success } = await env . CREATE_DB_RATE_LIMITER . limit ( { key : request . url } ) ;
18+ // --- Rate limiting
19+ const url = new URL ( request . url ) ;
2020
21- if ( ! success ) {
22- return new Response ( `429 Failure - rate limit exceeded for ${ request . url } ` , { status : 429 } ) ;
21+ // Prefer Cloudflare IP, then common proxy headers; fallback keeps key stable
22+ const clientIP =
23+ request . headers . get ( 'cf-connecting-ip' ) ||
24+ request . headers . get ( 'x-forwarded-for' ) ?. split ( ',' ) [ 0 ] ?. trim ( ) ||
25+ request . headers . get ( 'x-real-ip' ) ||
26+ 'unknown-ip' ;
27+
28+ // Key by IP + route so /health doesn't burn the same budget as /create
29+ let success = true ;
30+
31+ try {
32+ const res = await env . CREATE_DB_RATE_LIMITER . limit ( {
33+ key : `${ request . method } :${ clientIP } :${ url . pathname } ` ,
34+ } ) ;
35+ success = res . success ;
36+ } catch ( e ) {
37+ // Keep it simple: don't block users if the limiter is unavailable.
38+ // Flip to `success = false` if you prefer fail-closed.
39+ console . error ( 'Rate limiter error:' , e ) ;
40+ success = true ;
2341 }
2442
25- const url = new URL ( request . url ) ;
43+ if ( ! success ) {
44+ return new Response (
45+ JSON . stringify ( {
46+ error : 'rate_limited' ,
47+ message : 'Rate limit exceeded. Please try again later.' ,
48+ path : url . pathname ,
49+ ip : clientIP ,
50+ } ) ,
51+ {
52+ status : 429 ,
53+ headers : {
54+ 'Content-Type' : 'application/json' ,
55+ } ,
56+ } ,
57+ ) ;
58+ }
2659
2760 // --- Test endpoint for rate limit testing ---
2861 if ( url . pathname === '/test' && request . method === 'GET' ) {
0 commit comments