Skip to content

Add rate limiting to Cloud Functions#166

Open
Hell1213 wants to merge 1 commit into
roshankumar0036singh:mainfrom
Hell1213:fix/add-rate-limiting-issue-155
Open

Add rate limiting to Cloud Functions#166
Hell1213 wants to merge 1 commit into
roshankumar0036singh:mainfrom
Hell1213:fix/add-rate-limiting-issue-155

Conversation

@Hell1213
Copy link
Copy Markdown
Contributor

@Hell1213 Hell1213 commented May 22, 2026

Description

Added rate limiting to Cloud Functions to prevent DoS attacks and abuse. Implemented Firestore-based tracking that works with Firebase's https.onCall functions.

  • Implemented Firestore-based rate limiting for setRole function
  • Added automatic cleanup to prevent data bloat
  • Applied 10 requests/minute limit for admin operations
  • Graceful error handling with retry-after messaging

Fixes #155

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

  • Built successfully with npm run build
  • Verified TypeScript compilation with no errors
  • Tested rate limiting logic with sliding window approach
  • Confirmed cleanup function scheduled correctly

Manual Testing Plan:

  1. Deploy to Firebase emulator
  2. Call setRole 10 times rapidly
  3. Verify 11th call returns rate limit error
  4. Wait 1 minute and confirm function works again

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

Release Notes

  • New Features

    • Rate limiting controls now in place for Cloud Functions, enforcing separate request thresholds for admin operations, regular writes, and read operations
  • Chores

    • Implemented automatic hourly background cleanup of expired rate limit records for system efficiency
    • Updated TypeScript configuration to exclude test files from the build compilation process

Review Change Stack

- Implemented Firestore-based rate limiting for setRole function
- Added automatic cleanup to prevent data bloat
- Applied 10 requests/minute limit for admin operations
- Graceful error handling with retry-after messaging

Fixes roshankumar0036singh#155
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

This PR implements Firestore-backed rate limiting for Cloud Functions to prevent abuse and DoS attacks. It adds a configurable rate limiter middleware with rolling-window request tracking per user per function, integrates it into the setRole function, includes a scheduled cleanup job for stale records, and updates the build configuration to exclude tests.

Changes

Rate Limiting Feature

Layer / File(s) Summary
Rate Limiter Middleware: Config and Enforcement
cloud-functions/src/middleware/rateLimiter.ts
RateLimitConfig interface and RATE_LIMITS constant define window durations and max request thresholds for admin writes, writes, and reads. checkRateLimit computes rolling time windows, reads stored request timestamps from Firestore, filters to the current window, throws HttpsError("resource-exhausted") with retry-after metadata when limits are exceeded, and persists request timestamps and last-updated fields.
Stale Record Cleanup and Scheduled Job
cloud-functions/src/index.ts, cloud-functions/src/middleware/rateLimiter.ts
cleanupOldRateLimits queries and batch-deletes rate limit documents older than a cutoff (default 1 hour), logging the count removed. cleanupRateLimits is exported as a Pub/Sub scheduled Cloud Function that runs hourly and invokes the cleanup.
Rate Limiting Integration: setRole Example
cloud-functions/src/setRole.ts
Imports and calls checkRateLimit with the caller's UID, "setRole" function name, and ADMIN_WRITE configuration before executing authorization and role-setting logic.
TypeScript Build Configuration
cloud-functions/tsconfig.json
Excludes test files under src/**/__tests__ from compilation.

Sequence Diagram

sequenceDiagram
  participant SetRole
  participant CheckRateLimit
  participant Firestore
  participant ErrorHandler
  SetRole->>CheckRateLimit: checkRateLimit(userId, "setRole", ADMIN_WRITE)
  CheckRateLimit->>Firestore: query rateLimits document
  alt Document exists
    Firestore-->>CheckRateLimit: existing requests array
    CheckRateLimit->>CheckRateLimit: filter in-window timestamps
    alt Limit exceeded
      CheckRateLimit->>ErrorHandler: throw HttpsError("resource-exhausted")
      ErrorHandler-->>SetRole: HttpsError with retryAfter, limit, window
    else Limit not exceeded
      CheckRateLimit->>Firestore: append timestamp, update lastUpdated
      Firestore-->>CheckRateLimit: write complete
      CheckRateLimit-->>SetRole: void (continue)
    end
  else Document missing
    CheckRateLimit->>Firestore: initialize new document with timestamp
    Firestore-->>CheckRateLimit: write complete
    CheckRateLimit-->>SetRole: void (continue)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The rate limiter hops in with grace,
Timestamps and windows keep spam out of place,
Firestore buckets, per user defined,
Stale records cleaned on a schedule designed—
No DoS attacks shall win the race!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses issue #155: it implements rate limiting for setRole only and includes cleanup, but does not apply limits to all callable functions (createEvent, updateAttendance, sendNotification) or return 429 responses as specified. Apply rate limiting to all HTTP callable functions specified in issue #155 and ensure 429 status responses with retry-after headers are returned on violations.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add rate limiting to Cloud Functions' accurately summarizes the main change of implementing rate limiting middleware and scheduled cleanup.
Out of Scope Changes check ✅ Passed The tsconfig.json exclusion of test files is a reasonable tooling addition aligned with preventing test compilation, and the rate limiter implementation itself is in-scope for the stated objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cloud-functions/src/setRole.ts (1)

3-21: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure rate limiting is applied beyond setRole (issue #155 scope)

Only cloud-functions/src/setRole.ts calls checkRateLimit(...); the other callable functions (calculateReputation, getTopContributors, sendDailyDigest) don’t appear to be rate-limited. Also, I can’t find createEvent / updateAttendance / sendNotification in this repo by name—please verify the intended issue #155 endpoints are covered (or explicitly document this as a phased rollout).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/setRole.ts` around lines 3 - 21, The PR only applies rate
limiting in setRole via checkRateLimit(context.auth.uid, "setRole",
RATE_LIMITS.ADMIN_WRITE); ensure the other callable functions referenced in
issue `#155`—namely calculateReputation, getTopContributors, sendDailyDigest (and
if present: createEvent, updateAttendance, sendNotification)—also call
checkRateLimit with an appropriate key and RATE_LIMITS constant; locate each
function (e.g., calculateReputation, getTopContributors, sendDailyDigest) and
add the same auth check plus await checkRateLimit(context.auth.uid,
"<functionName>", <appropriate RATE_LIMITS.*>) at the top, or if those endpoints
are intentionally excluded, add a short README note documenting this as a phased
rollout and listing which functions remain unrate-limited.
🧹 Nitpick comments (2)
cloud-functions/src/middleware/rateLimiter.ts (2)

42-42: ⚡ Quick win

Prefer optional chaining for conciseness.

As per the SonarCloud hint, use optional chaining to simplify the null/undefined check.

♻️ Proposed refactor
-      if (data && data.requests) {
+      if (data?.requests) {
         const recentRequests = (data.requests as number[])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` at line 42, The conditional in
rateLimiter.ts uses a manual null/undefined check ("if (data && data.requests)
{"); replace it with optional chaining to simplify and clarify the intent by
changing the condition to check data?.requests instead, updating the conditional
in the function/block that references the variable "data" (the rate limiter
request-count check) so it uses optional chaining.

24-85: ⚡ Quick win

Consider logging when rate limits are exceeded.

The PR objectives mention "log violations for monitoring," but the current implementation only logs errors (line 83) and cleanup counts. Adding a log statement when the rate limit is actually hit (lines 51-59) would help detect abuse patterns and monitor the effectiveness of rate limiting.

📊 Proposed addition
         if (recentRequests.length >= config.maxRequests) {
           const oldestRequest = recentRequests[0];
           const retryAfterMs = oldestRequest + config.windowMs - now;
           const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
           
+          console.warn(`Rate limit exceeded for user ${userId} on function ${functionName}. ` +
+            `Limit: ${config.maxRequests} requests per ${config.windowMs / 1000}s`);
+          
           throw new functions.https.HttpsError(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` around lines 24 - 85, The
rate-limit branch that throws the functions.https.HttpsError in checkRateLimit
currently doesn't emit a monitoring log; add a structured log just before
throwing (using the existing logging approach, e.g., console.warn or the app's
logger) that records userId, functionName, retryAfterSeconds, config.maxRequests
and config.windowMs (or seconds) and any identifying doc id (rateLimitRef.id) to
surface violations for monitoring and analytics; ensure the log is emitted only
in the exhausted-rate branch (the block that computes retryAfterSeconds and
throws) so callers still receive the same HttpsError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cloud-functions/src/index.ts`:
- Around line 16-21: The schedule string passed to functions.pubsub.schedule in
the cleanupRateLimits export is grammatically incorrect ("every 1 hours");
update the schedule argument to a correct form such as "every 1 hour" or simply
"every hour" in the cleanupRateLimits definition so the call to .schedule(...)
reads correctly while leaving the async onRun handler and cleanupOldRateLimits()
call unchanged.

In `@cloud-functions/src/middleware/rateLimiter.ts`:
- Around line 47-49: The retry-after calculation using oldestRequest,
retryAfterMs and retryAfterSeconds can yield zero or negative values; change the
computation so retryAfterSeconds = Math.max(1, Math.ceil((oldestRequest +
config.windowMs - now) / 1000)) to ensure the header is always at least 1
second; update the place where retryAfterSeconds is used to rely on this bounded
value.
- Around line 79-84: The catch block in the rate limiter swallows non-HttpsError
exceptions (current code checks functions.https.HttpsError then logs and
returns), which allows requests to bypass limits; change it to fail-closed by
rethrowing or converting every error into an HttpsError: in the catch in
rateLimiter middleware, after logging rethrow the original error or throw a new
functions.https.HttpsError('internal', 'Rate limit check failed', { message:
String(error) }) so any Firestore/network/permission failure aborts the request
instead of silently permitting it.
- Around line 88-108: The current cleanupOldRateLimits function only deletes up
to 500 documents because of the .limit(500) query; change it to repeatedly query
and delete in batches until no stale docs remain. In cleanupOldRateLimits, wrap
the query+batch-delete logic (the
admin.firestore().collection("rateLimits").where("lastUpdated", "<",
cutoffTime).limit(500).get() call, snapshot handling, and batch.commit()) in a
loop (e.g., while/for) that exits when snapshot.empty, so you run successive
queries and commits until all matching documents are removed; keep using
batch.delete per doc and batch.commit for each batch. Ensure you await each
batch.commit before re-querying to avoid race conditions.
- Around line 37-78: The current read-modify-write on rateLimitRef can race—wrap
the logic inside a Firestore transaction (firestore.runTransaction) and perform
transaction.get(rateLimitRef), compute recentRequests = (data.requests as
number[]).filter(...) within the transaction, check recentRequests.length
against config.maxRequests and if exceeded throw the same
functions.https.HttpsError, otherwise push now and use
transaction.set(rateLimitRef, { requests: recentRequests, lastUpdated: now })
(or create the doc if missing) so the get+check+write is atomic; keep using
config.maxRequests and config.windowMs and preserve the retryAfter calculation
and metadata fields.

---

Outside diff comments:
In `@cloud-functions/src/setRole.ts`:
- Around line 3-21: The PR only applies rate limiting in setRole via
checkRateLimit(context.auth.uid, "setRole", RATE_LIMITS.ADMIN_WRITE); ensure the
other callable functions referenced in issue `#155`—namely calculateReputation,
getTopContributors, sendDailyDigest (and if present: createEvent,
updateAttendance, sendNotification)—also call checkRateLimit with an appropriate
key and RATE_LIMITS constant; locate each function (e.g., calculateReputation,
getTopContributors, sendDailyDigest) and add the same auth check plus await
checkRateLimit(context.auth.uid, "<functionName>", <appropriate RATE_LIMITS.*>)
at the top, or if those endpoints are intentionally excluded, add a short README
note documenting this as a phased rollout and listing which functions remain
unrate-limited.

---

Nitpick comments:
In `@cloud-functions/src/middleware/rateLimiter.ts`:
- Line 42: The conditional in rateLimiter.ts uses a manual null/undefined check
("if (data && data.requests) {"); replace it with optional chaining to simplify
and clarify the intent by changing the condition to check data?.requests
instead, updating the conditional in the function/block that references the
variable "data" (the rate limiter request-count check) so it uses optional
chaining.
- Around line 24-85: The rate-limit branch that throws the
functions.https.HttpsError in checkRateLimit currently doesn't emit a monitoring
log; add a structured log just before throwing (using the existing logging
approach, e.g., console.warn or the app's logger) that records userId,
functionName, retryAfterSeconds, config.maxRequests and config.windowMs (or
seconds) and any identifying doc id (rateLimitRef.id) to surface violations for
monitoring and analytics; ensure the log is emitted only in the exhausted-rate
branch (the block that computes retryAfterSeconds and throws) so callers still
receive the same HttpsError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e82758f-2272-4299-8b7a-7f890fd2bd63

📥 Commits

Reviewing files that changed from the base of the PR and between 76e5d71 and 1a0720a.

📒 Files selected for processing (4)
  • cloud-functions/src/index.ts
  • cloud-functions/src/middleware/rateLimiter.ts
  • cloud-functions/src/setRole.ts
  • cloud-functions/tsconfig.json

Comment on lines +16 to +21
export const cleanupRateLimits = functions.pubsub
.schedule("every 1 hours")
.onRun(async () => {
await cleanupOldRateLimits();
return null;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix grammatical error in schedule string.

Line 17 uses "every 1 hours" which is grammatically incorrect.

📝 Proposed fix
 export const cleanupRateLimits = functions.pubsub
-  .schedule("every 1 hours")
+  .schedule("every 1 hour")
   .onRun(async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const cleanupRateLimits = functions.pubsub
.schedule("every 1 hours")
.onRun(async () => {
await cleanupOldRateLimits();
return null;
});
export const cleanupRateLimits = functions.pubsub
.schedule("every 1 hour")
.onRun(async () => {
await cleanupOldRateLimits();
return null;
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/index.ts` around lines 16 - 21, The schedule string
passed to functions.pubsub.schedule in the cleanupRateLimits export is
grammatically incorrect ("every 1 hours"); update the schedule argument to a
correct form such as "every 1 hour" or simply "every hour" in the
cleanupRateLimits definition so the call to .schedule(...) reads correctly while
leaving the async onRun handler and cleanupOldRateLimits() call unchanged.

Comment on lines +37 to +78
const doc = await rateLimitRef.get();

if (doc.exists) {
const data = doc.data();

if (data && data.requests) {
const recentRequests = (data.requests as number[])
.filter((timestamp: number) => timestamp > windowStart);

if (recentRequests.length >= config.maxRequests) {
const oldestRequest = recentRequests[0];
const retryAfterMs = oldestRequest + config.windowMs - now;
const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);

throw new functions.https.HttpsError(
"resource-exhausted",
`Rate limit exceeded. You can try again in ${retryAfterSeconds} seconds.`,
{
retryAfter: retryAfterSeconds,
limit: config.maxRequests,
window: config.windowMs / 1000
}
);
}

recentRequests.push(now);
await rateLimitRef.set({
requests: recentRequests,
lastUpdated: now
});
} else {
await rateLimitRef.set({
requests: [now],
lastUpdated: now
});
}
} else {
await rateLimitRef.set({
requests: [now],
lastUpdated: now
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical race condition allows rate limit bypass.

The read-modify-write pattern (get document → filter/update array → set document) is not atomic. When multiple requests arrive concurrently for the same user+function, they can all read the same state, each determine they're under the limit, and all write back successfully—bypassing the rate limit entirely.

Use a Firestore transaction to ensure atomicity.

🔒 Proposed fix using a transaction
-  try {
-    const doc = await rateLimitRef.get();
-    
-    if (doc.exists) {
-      const data = doc.data();
-      
-      if (data && data.requests) {
-        const recentRequests = (data.requests as number[])
-          .filter((timestamp: number) => timestamp > windowStart);
-        
-        if (recentRequests.length >= config.maxRequests) {
-          const oldestRequest = recentRequests[0];
-          const retryAfterMs = oldestRequest + config.windowMs - now;
-          const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
-          
-          throw new functions.https.HttpsError(
-            "resource-exhausted",
-            `Rate limit exceeded. You can try again in ${retryAfterSeconds} seconds.`,
-            {
-              retryAfter: retryAfterSeconds,
-              limit: config.maxRequests,
-              window: config.windowMs / 1000
-            }
-          );
-        }
-        
-        recentRequests.push(now);
-        await rateLimitRef.set({
-          requests: recentRequests,
-          lastUpdated: now
-        });
-      } else {
-        await rateLimitRef.set({
-          requests: [now],
-          lastUpdated: now
-        });
-      }
-    } else {
-      await rateLimitRef.set({
-        requests: [now],
-        lastUpdated: now
-      });
-    }
+  try {
+    await admin.firestore().runTransaction(async (transaction) => {
+      const doc = await transaction.get(rateLimitRef);
+      
+      let recentRequests: number[] = [];
+      
+      if (doc.exists) {
+        const data = doc.data();
+        if (data?.requests) {
+          recentRequests = (data.requests as number[])
+            .filter((timestamp: number) => timestamp > windowStart);
+        }
+      }
+      
+      if (recentRequests.length >= config.maxRequests) {
+        const oldestRequest = recentRequests[0];
+        const retryAfterMs = oldestRequest + config.windowMs - now;
+        const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
+        
+        throw new functions.https.HttpsError(
+          "resource-exhausted",
+          `Rate limit exceeded. You can try again in ${retryAfterSeconds} seconds.`,
+          {
+            retryAfter: retryAfterSeconds,
+            limit: config.maxRequests,
+            window: config.windowMs / 1000
+          }
+        );
+      }
+      
+      recentRequests.push(now);
+      transaction.set(rateLimitRef, {
+        requests: recentRequests,
+        lastUpdated: now
+      });
+    });
   } catch (error) {
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 42-42: Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=roshankumar0036singh_Uni-Event&issues=AZ5OoxB7_luB5t_7288e&open=AZ5OoxB7_luB5t_7288e&pullRequest=166

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` around lines 37 - 78, The
current read-modify-write on rateLimitRef can race—wrap the logic inside a
Firestore transaction (firestore.runTransaction) and perform
transaction.get(rateLimitRef), compute recentRequests = (data.requests as
number[]).filter(...) within the transaction, check recentRequests.length
against config.maxRequests and if exceeded throw the same
functions.https.HttpsError, otherwise push now and use
transaction.set(rateLimitRef, { requests: recentRequests, lastUpdated: now })
(or create the doc if missing) so the get+check+write is atomic; keep using
config.maxRequests and config.windowMs and preserve the retryAfter calculation
and metadata fields.

Comment on lines +47 to +49
const oldestRequest = recentRequests[0];
const retryAfterMs = oldestRequest + config.windowMs - now;
const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Retry-after calculation can produce negative or zero values.

If the oldest request is about to expire, retryAfterMs can be negative or very small. Math.ceil of a negative number remains negative, which is invalid for a retry-after header.

Use Math.max(1, ...) to ensure at least 1 second.

⏱️ Proposed fix
         const oldestRequest = recentRequests[0];
         const retryAfterMs = oldestRequest + config.windowMs - now;
-        const retryAfterSeconds = Math.ceil(retryAfterMs / 1000);
+        const retryAfterSeconds = Math.max(1, Math.ceil(retryAfterMs / 1000));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` around lines 47 - 49, The
retry-after calculation using oldestRequest, retryAfterMs and retryAfterSeconds
can yield zero or negative values; change the computation so retryAfterSeconds =
Math.max(1, Math.ceil((oldestRequest + config.windowMs - now) / 1000)) to ensure
the header is always at least 1 second; update the place where retryAfterSeconds
is used to rely on this bounded value.

Comment on lines +79 to +84
} catch (error) {
if (error instanceof functions.https.HttpsError) {
throw error;
}
console.error("Rate limit check failed:", error);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Silent failure defeats rate limiting security.

Non-HttpsError exceptions (Firestore permissions, network failures, etc.) are logged but suppressed, allowing the request to proceed without rate limiting. This defeats the security purpose—if Firestore is unavailable or misconfigured, all rate limits are bypassed.

Either rethrow all errors to fail-closed, or use an in-memory fallback.

🛡️ Proposed fix: fail-closed by rethrowing all errors
   } catch (error) {
     if (error instanceof functions.https.HttpsError) {
       throw error;
     }
     console.error("Rate limit check failed:", error);
+    throw new functions.https.HttpsError(
+      "internal",
+      "Rate limit check failed. Please try again.",
+    );
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` around lines 79 - 84, The
catch block in the rate limiter swallows non-HttpsError exceptions (current code
checks functions.https.HttpsError then logs and returns), which allows requests
to bypass limits; change it to fail-closed by rethrowing or converting every
error into an HttpsError: in the catch in rateLimiter middleware, after logging
rethrow the original error or throw a new functions.https.HttpsError('internal',
'Rate limit check failed', { message: String(error) }) so any
Firestore/network/permission failure aborts the request instead of silently
permitting it.

Comment on lines +88 to +108
export async function cleanupOldRateLimits(olderThanMs: number = 60 * 60 * 1000): Promise<void> {
const cutoffTime = Date.now() - olderThanMs;

const snapshot = await admin.firestore()
.collection("rateLimits")
.where("lastUpdated", "<", cutoffTime)
.limit(500)
.get();

if (snapshot.empty) {
return;
}

const batch = admin.firestore().batch();
snapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});

await batch.commit();
console.log(`Cleaned up ${snapshot.size} old rate limit records`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete cleanup when more than 500 stale records exist.

The function queries with .limit(500) and processes only that batch. If there are more than 500 stale records, they won't be cleaned up in this run. Given the hourly schedule, if stale records accumulate faster than 500/hour, they'll build up indefinitely, leading to storage bloat and degraded query performance.

Either run in a loop until all stale records are cleaned, or document this limitation and monitor for accumulation.

🔄 Proposed fix: loop until all stale records are cleaned
 export async function cleanupOldRateLimits(olderThanMs: number = 60 * 60 * 1000): Promise<void> {
   const cutoffTime = Date.now() - olderThanMs;
+  let totalCleaned = 0;
+  let hasMore = true;
   
-  const snapshot = await admin.firestore()
-    .collection("rateLimits")
-    .where("lastUpdated", "<", cutoffTime)
-    .limit(500)
-    .get();
-  
-  if (snapshot.empty) {
-    return;
-  }
-  
-  const batch = admin.firestore().batch();
-  snapshot.docs.forEach(doc => {
-    batch.delete(doc.ref);
-  });
-  
-  await batch.commit();
-  console.log(`Cleaned up ${snapshot.size} old rate limit records`);
+  while (hasMore) {
+    const snapshot = await admin.firestore()
+      .collection("rateLimits")
+      .where("lastUpdated", "<", cutoffTime)
+      .limit(500)
+      .get();
+    
+    if (snapshot.empty) {
+      hasMore = false;
+      break;
+    }
+    
+    const batch = admin.firestore().batch();
+    snapshot.docs.forEach(doc => {
+      batch.delete(doc.ref);
+    });
+    
+    await batch.commit();
+    totalCleaned += snapshot.size;
+    
+    // If we got fewer than 500, we're done
+    if (snapshot.size < 500) {
+      hasMore = false;
+    }
+  }
+  
+  if (totalCleaned > 0) {
+    console.log(`Cleaned up ${totalCleaned} old rate limit records`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function cleanupOldRateLimits(olderThanMs: number = 60 * 60 * 1000): Promise<void> {
const cutoffTime = Date.now() - olderThanMs;
const snapshot = await admin.firestore()
.collection("rateLimits")
.where("lastUpdated", "<", cutoffTime)
.limit(500)
.get();
if (snapshot.empty) {
return;
}
const batch = admin.firestore().batch();
snapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});
await batch.commit();
console.log(`Cleaned up ${snapshot.size} old rate limit records`);
}
export async function cleanupOldRateLimits(olderThanMs: number = 60 * 60 * 1000): Promise<void> {
const cutoffTime = Date.now() - olderThanMs;
let totalCleaned = 0;
let hasMore = true;
while (hasMore) {
const snapshot = await admin.firestore()
.collection("rateLimits")
.where("lastUpdated", "<", cutoffTime)
.limit(500)
.get();
if (snapshot.empty) {
hasMore = false;
break;
}
const batch = admin.firestore().batch();
snapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});
await batch.commit();
totalCleaned += snapshot.size;
// If we got fewer than 500, we're done
if (snapshot.size < 500) {
hasMore = false;
}
}
if (totalCleaned > 0) {
console.log(`Cleaned up ${totalCleaned} old rate limit records`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-functions/src/middleware/rateLimiter.ts` around lines 88 - 108, The
current cleanupOldRateLimits function only deletes up to 500 documents because
of the .limit(500) query; change it to repeatedly query and delete in batches
until no stale docs remain. In cleanupOldRateLimits, wrap the query+batch-delete
logic (the admin.firestore().collection("rateLimits").where("lastUpdated", "<",
cutoffTime).limit(500).get() call, snapshot handling, and batch.commit()) in a
loop (e.g., while/for) that exits when snapshot.empty, so you run successive
queries and commits until all matching documents are removed; keep using
batch.delete per doc and batch.commit for each batch. Ensure you await each
batch.commit before re-querying to avoid race conditions.

@roshankumar0036singh
Copy link
Copy Markdown
Owner

roshankumar0036singh commented May 24, 2026

@Hell1213 handle the coderabbit suggestion wherever necessary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rate limiting to Cloud Functions

2 participants