Skip to content

Add rate limit to write endpoints#45

Merged
MrImmortal09 merged 9 commits into
iiitl:mainfrom
Ewan-Dkhar:no-type/rate-limit-middleware
Apr 12, 2026
Merged

Add rate limit to write endpoints#45
MrImmortal09 merged 9 commits into
iiitl:mainfrom
Ewan-Dkhar:no-type/rate-limit-middleware

Conversation

@Ewan-Dkhar

@Ewan-Dkhar Ewan-Dkhar commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Description

Added rate limit for write endpoints. This is important to implement as some people can abuse the api and call the api thousands of times. So inorder for that to not happen, the use of rate limiters is crucial.

Closes #29


Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactor / code style

Checklist

  • I have read CONTRIBUTING.md
  • My branch follows the naming convention (feat/, fix/, docs/, etc.)
  • My commits follow the Conventional Commits format
  • I have run pnpm lint and there are no errors
  • I have tested my changes locally (pnpm dev)
  • I have updated documentation where necessary
  • New and existing UI uses design-system tokens (no arbitrary hex values)
  • I have linked the related issue above

Screenshots / Screen Recording

Before After
N/A N/A

Summary by CodeRabbit

  • New Features

    • API rate limiting: endpoints now limit to 5 requests per minute per IP. Excess requests receive a 429 (Too Many Requests) response with a Retry-After header and a JSON error message.
  • Chores

    • .env.example updated to include commented Upstash Redis configuration variables to enable rate limiting when configured.

@mini-walkerx mini-walkerx Bot mentioned this pull request Apr 12, 2026
4 tasks
@vercel

vercel Bot commented Apr 12, 2026

Copy link
Copy Markdown

@Ewan-Dkhar is attempting to deploy a commit to the mrimmortal09's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Upstash Redis configuration and dependencies, implements a sliding-window rate limiter (Upstash) and a Next.js route-handler wrapper enforcing per-route/IP limits (5 requests per minute) returning 429 with Retry-After when breached.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
​.env.example, package.json
Added commented Upstash env vars (UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN) and runtime deps @upstash/ratelimit, @upstash/redis.
Rate Limiting Core
lib/ratelimit.ts
New exported limit(key, config) using Upstash Ratelimit with an Upstash Redis backend; memoizes Ratelimit instances, bypasses when credentials absent (logs warning), computes retryAfter, increments breach_count:${key} on violations and logs a structured event when breaches reach 3, and returns structured limit results.
Rate Limiting Middleware
lib/with-ratelimit.ts
New exported withRateLimit HOF for Next.js route handlers: extracts client IP, builds pathname:ip key, enforces { maxRequests: 5, window: "1 m" }, returns 429 JSON with Retry-After when limit fails, otherwise delegates to the wrapped handler.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as withRateLimit
    participant Limiter as lib/ratelimit.ts
    participant Redis as Upstash\ Redis

    Client->>Handler: HTTP request
    Handler->>Handler: Extract IP & pathname\nConstruct key (pathname:ip)
    Handler->>Limiter: limit(key, {maxRequests:5, window:"1 m"})
    alt UPSTASH credentials present
        Limiter->>Redis: Ratelimit check (sliding window)
        Redis-->>Limiter: { success, reset, remaining }
        alt success == true
            Limiter-->>Handler: { success: true, ... }
            Handler->>Handler: Call original handler
            Handler-->>Client: Handler response
        else success == false
            Limiter->>Redis: INCR breach_count:key
            Limiter->>Redis: EXPIRE breach_count:key 3600
            Limiter-->>Handler: { success: false, retryAfter }
            Handler-->>Client: 429 JSON + Retry-After header
        end
    else Missing UPSTASH credentials
        Limiter-->>Handler: { success: true, limit, remaining } (bypass)
        Handler->>Handler: Call original handler
        Handler-->>Client: Handler response
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially satisfies issue #29: it exports lib/ratelimit.ts with a limit(key, config) helper and includes rate limit configuration, but does not apply rate limiting to the seven specified write endpoints or implement AuditLog logging for consistent breaches. Apply the rate limit middleware to all seven write endpoints (magic link, sign-up, recovery, contact form, job posting, report, avatar upload) and implement AuditLog logging when breach count reaches three.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 accurately describes the main change: adding rate limiting functionality to write endpoints as evidenced by the new rate limit modules and middleware wrapper.
Out of Scope Changes check ✅ Passed All changes are in-scope: new rate limit module, middleware wrapper, dependencies, and configuration are all necessary to support the rate limiting feature for write endpoints.

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Line 9: Add a final newline at end of file so the .env example ends with a
blank line to satisfy dotenv-linter's EndingBlankLine rule; specifically ensure
the file ends with a trailing newline after the UPSTASH_REDIS_REST_TOKEN= line
(the EOF for the .env example).

In `@app/api/health/route.ts`:
- Around line 17-30: Remove or disable the stub write handlers on the health
endpoint: instead of keeping the POST/PUT/DELETE exports that return a 200
success body, either delete the exported handlers (POST, PUT, DELETE) or change
them to return a 405 Method Not Allowed response (using the same withRateLimit
wrapper) so the health route only supports safe read semantics; update the
POST/PUT/DELETE functions that currently call Response.json({ success: true })
to instead return a Response with status 405 and an appropriate Allow header or
remove those exports entirely until real write logic is implemented.

In `@lib/audit-log.ts`:
- Around line 10-26: logToAudit currently only logs to console and the
persistence path is commented out; implement persistence by ensuring a MongoDB
connection (call or re-enable connectDB) and un-comment/use the AuditLog model
to create records (use AuditLog.create with fields severity, event, target,
message, timestamp). Locate the logToAudit function and the commented block that
references connectDB and AuditLog.create, re-enable connection logic and perform
AuditLog.create({ severity: params.severity, event: params.event, target:
params.target, message: params.message, timestamp: new Date() }) and handle
errors in the try/catch so failures are logged and do not crash the process.

In `@lib/ratelimit.ts`:
- Around line 5-8: The code uses non-null assertions when constructing the Redis
client (new Redis) with process.env.UPSTASH_REDIS_REST_URL and
process.env.UPSTASH_REDIS_REST_TOKEN which delays configuration errors to
runtime; instead add an explicit startup validation: check
process.env.UPSTASH_REDIS_REST_URL and process.env.UPSTASH_REDIS_REST_TOKEN and
if either is missing throw a clear Error (or log and exit) before calling new
Redis so the failure is fast and the Redis instance is only created when both
env vars are present.
- Around line 43-49: The current block logs CONSISTENT_RATE_LIMIT_BREACH every
time breachCount >= 3 causing audit floods; change it to escalate only once by
gating the log with a short-lived flag in your rate-store (e.g., an "escalated"
key) or a cooldown check before calling logToAudit: when breachCount reaches the
threshold (breachCount === 3) or when no "escalated" flag exists for the target
key, call logToAudit and then set the "escalated" flag with a TTL (e.g., 1
hour); reference breachCount, logToAudit, CONSISTENT_RATE_LIMIT_BREACH and key
to locate and update the logic so subsequent blocked requests skip logging until
the TTL expires.

In `@lib/with-ratelimit.ts`:
- Around line 12-15: The call to limit(ip, {...}) can throw and currently will
bubble up causing 500s; wrap that invocation in a try/catch inside the
withRateLimit (with-ratelimit) wrapper, catch errors from limit, log the error
(include the error object and context like ip and options), and apply an
explicit fallback policy (e.g., fail-open: set success = true and retryAfter = 0
to allow requests during backend outage, or fail-closed: set success = false and
a conservative retryAfter) so downstream code can continue to make a
deterministic decision; ensure you reference and override the same local
variables used later (success, retryAfter) so the rest of the handler uses the
fallback values.
- Line 9: The client key extraction in the with-rate-limit logic uses a raw
header and a shared "127.0.0.1" fallback which can broaden/poison throttling;
change how const ip is derived by parsing req.headers.get("x-forwarded-for") to
take the first comma-separated value and trim it, validate it (e.g., via
net.isIP) and only use it if valid; if missing/invalid, fall back to
req.headers.get("x-real-ip") or req.socket?.remoteAddress (again validating) and
do not default to a global "127.0.0.1" string—use undefined or a per-request
unique identifier instead so the rate limiter key (ip) remains specific and not
shared.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f224a92-a28b-4da7-b008-515dd50d94f2

📥 Commits

Reviewing files that changed from the base of the PR and between c9b3504 and ba12d90.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .env.example
  • app/api/health/route.ts
  • lib/audit-log.ts
  • lib/ratelimit.ts
  • lib/with-ratelimit.ts
  • package.json

Comment thread .env.example Outdated
Comment thread app/api/health/route.ts Outdated
Comment thread lib/audit-log.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/with-ratelimit.ts
Comment thread lib/with-ratelimit.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 12-15: Reorder and reformat the Mailgun block so dotenv-linter
passes: place MAILGUN_API_KEY, MAILGUN_DOMAIN and optional MAILGUN_URL first,
then EMAIL_FROM last, and remove the surrounding quotes from the EMAIL_FROM
value (use EMAIL_FROM=no-reply@yourdomain.com); ensure MAILGUN_URL remains
commented if unused.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5ac5090e-a62e-4d86-9419-8704248fcbad

📥 Commits

Reviewing files that changed from the base of the PR and between ba12d90 and 2c740bb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • .env.example
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

Comment thread .env.example

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/ratelimit.ts (1)

5-8: ⚠️ Potential issue | 🟠 Major

Fail fast when Upstash env vars are missing.

Line 6 and Line 7 still rely on non-null assertions, so misconfiguration is deferred and less clear at runtime. Validate once before constructing Redis.

Suggested fix
+const upstashUrl = process.env.UPSTASH_REDIS_REST_URL;
+const upstashToken = process.env.UPSTASH_REDIS_REST_TOKEN;
+
+if (!upstashUrl || !upstashToken) {
+  throw new Error('Missing UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN');
+}
+
 const redis = new Redis({
-  url: process.env.UPSTASH_REDIS_REST_URL!,
-  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
+  url: upstashUrl,
+  token: upstashToken,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 5 - 8, The code currently uses non-null
assertions when constructing Redis (const redis = new Redis({...})) which defers
misconfiguration; before calling new Redis, read
process.env.UPSTASH_REDIS_REST_URL and process.env.UPSTASH_REDIS_REST_TOKEN into
local variables, validate they are present (throw new Error or log and
process.exit with a clear message mentioning
UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN), and then pass those validated
variables into new Redis instead of using the ! operator.
🧹 Nitpick comments (1)
lib/ratelimit.ts (1)

21-25: Cache Ratelimit instances instead of recreating per request.

Constructing a new limiter on every limit(...) call adds avoidable overhead. A small module-level cache keyed by {maxRequests, window} keeps behavior the same with lower allocation/runtime cost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 21 - 25, The code currently creates a new
Ratelimit instance inside the request path (new Ratelimit({...}) using
Ratelimit.slidingWindow(...)), which causes unnecessary allocations; instead add
a small module-level cache keyed by the limiter configuration (maxRequests and
window) and reuse cached Ratelimit instances. Implement a map (e.g.
Map<string,Ratelimit>) keyed by `${config.maxRequests}:${config.window}` and
change the code that constructs `ratelimit` to first lookup the cache and only
call `new Ratelimit({...})` and Ratelimit.slidingWindow(...) when missing;
ensure the existing `limit(...)` call and analytics flag remain unchanged so
behavior is identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ratelimit.ts`:
- Around line 44-48: The audit call is logging the raw request identifier via
target: key; change the logToAudit invocation in the rate limiter (where
logToAudit is called) to pseudonymize the key first (e.g., compute a stable
one-way hash or HMAC of key with an application secret/salt) and pass that
pseudonymized value as target instead of the raw key; keep the original key only
in volatile memory for enforcement and never write it to audit logs or
persistent stores.

---

Duplicate comments:
In `@lib/ratelimit.ts`:
- Around line 5-8: The code currently uses non-null assertions when constructing
Redis (const redis = new Redis({...})) which defers misconfiguration; before
calling new Redis, read process.env.UPSTASH_REDIS_REST_URL and
process.env.UPSTASH_REDIS_REST_TOKEN into local variables, validate they are
present (throw new Error or log and process.exit with a clear message mentioning
UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN), and then pass those validated
variables into new Redis instead of using the ! operator.

---

Nitpick comments:
In `@lib/ratelimit.ts`:
- Around line 21-25: The code currently creates a new Ratelimit instance inside
the request path (new Ratelimit({...}) using Ratelimit.slidingWindow(...)),
which causes unnecessary allocations; instead add a small module-level cache
keyed by the limiter configuration (maxRequests and window) and reuse cached
Ratelimit instances. Implement a map (e.g. Map<string,Ratelimit>) keyed by
`${config.maxRequests}:${config.window}` and change the code that constructs
`ratelimit` to first lookup the cache and only call `new Ratelimit({...})` and
Ratelimit.slidingWindow(...) when missing; ensure the existing `limit(...)` call
and analytics flag remain unchanged so behavior is identical.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbacc699-eece-447a-88ad-92d8ba29ea2b

📥 Commits

Reviewing files that changed from the base of the PR and between 2c740bb and c557c24.

📒 Files selected for processing (2)
  • lib/ratelimit.ts
  • lib/with-ratelimit.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/with-ratelimit.ts

Comment thread lib/ratelimit.ts Outdated
@Ewan-Dkhar

Copy link
Copy Markdown
Contributor Author

@MrImmortal09 please review this pr

@whilstsomebody whilstsomebody left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like a good PR by a beginner but still needs a lot of improvements.

Fix the posted comment and tag me again for further reviews.

Comment thread lib/audit-log.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts Outdated
Comment thread lib/ratelimit.ts
Comment thread lib/ratelimit.ts
@MrImmortal09

MrImmortal09 commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

@whilstsomebody should this be consider a hard issue?

@whilstsomebody

Copy link
Copy Markdown
Collaborator

@whilstsomebody should this be consider a hard issue?

Yes, it's a hard issue. We are not going to merge this until a better rate limiting is reached.

@Ewan-Dkhar

Copy link
Copy Markdown
Contributor Author

@whilstsomebody please review my changes and do give me feedback on how to improve it more if needed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/ratelimit.ts (1)

56-63: ⚠️ Potential issue | 🟠 Major

Raw IP address logged in audit - PII concern persists.

The key contains the client IP (format: ${pathname}:${ip} from with-ratelimit.ts). Logging raw identifiers exposes PII in logs/audit records. Consider hashing or pseudonymizing the key before logging.

Suggested fix
+import { createHash } from 'crypto';
+
+function fingerprintKey(key: string): string {
+  return createHash('sha256').update(key).digest('hex').slice(0, 16);
+}
+
 // In the logging block:
       if (breachCount === 3) {
         console.log({
                     severity: 'WARNING',
                     event: 'CONSISTENT_RATE_LIMIT_BREACH',
-                    target: key,
-                    message: `Key breached rate limits ${breachCount} times within the hour.`,
+                    target: fingerprintKey(key),
+                    message: `Rate-limit key fingerprint breached limits ${breachCount} times within the hour.`,
                   });
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 56 - 63, The audit log currently emits the raw
`key` (which contains client IP) when `breachCount === 3`, exposing PII; update
the logging in the block that checks `breachCount` in ratelimit.ts to
pseudonymize the identifier (e.g., compute a stable hash or HMAC of `key` using
a server-side secret) and log that hashed value instead of `key` (keep the event
name `CONSISTENT_RATE_LIMIT_BREACH`, include `targetHash` or similar), ensuring
you reference the same symbols (`breachCount`, `key`) and do not change the
external log structure beyond replacing the raw identifier.
🧹 Nitpick comments (1)
lib/ratelimit.ts (1)

49-54: Sliding TTL for breach count may be intentional but worth noting.

pipeline.expire(breachKey, 3600) resets the TTL on every breach, meaning persistent abusers never have their breach count reset to zero. If a fixed 1-hour window from first breach is desired, use NX flag or EXPIREAT:

pipeline.expire(breachKey, 3600, 'NX'); // Only set if no TTL exists

Current behavior may be acceptable if you want to track cumulative abuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 49 - 54, The current pipeline that increments
breachKey and then sets expire on breachKey (see pipeline, breachKey,
pipeline.expire, results, breachCount) resets the TTL on every breach which
keeps the window sliding; if you want a fixed 1-hour window from the first
breach change the expire call to only set TTL when none exists (use the NX
option) or set an absolute expiry via EXPIREAT so the TTL is not refreshed on
each incr; update the pipeline.expire invocation to use the NX flag or switch to
EXPIREAT to enforce a fixed window instead of the current sliding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ratelimit.ts`:
- Around line 73-82: The catch block in the rate limiter (the catch around the
Redis call that currently logs in console.error and returns success: true)
implements a fail-open behavior that can disable rate limiting under Redis
failures; change it to a fail-closed conservative response by returning success:
false (or otherwise treat the request as limited) and set remaining: 0,
retryAfter: a conservative backoff/reset interval, and keep a clear error log
via the same logger used elsewhere; update the catch in the function handling
rate checks (the block that currently returns { success: true, limit:
config.maxRequests, remaining: config.maxRequests, reset: 0, retryAfter: 0 }) to
return the conservative values and/or trigger the circuit-breaker path, and if
you intentionally want fail-open instead, add an explicit comment above the
catch explaining the availability/security trade-off and why success: true is
used.

---

Duplicate comments:
In `@lib/ratelimit.ts`:
- Around line 56-63: The audit log currently emits the raw `key` (which contains
client IP) when `breachCount === 3`, exposing PII; update the logging in the
block that checks `breachCount` in ratelimit.ts to pseudonymize the identifier
(e.g., compute a stable hash or HMAC of `key` using a server-side secret) and
log that hashed value instead of `key` (keep the event name
`CONSISTENT_RATE_LIMIT_BREACH`, include `targetHash` or similar), ensuring you
reference the same symbols (`breachCount`, `key`) and do not change the external
log structure beyond replacing the raw identifier.

---

Nitpick comments:
In `@lib/ratelimit.ts`:
- Around line 49-54: The current pipeline that increments breachKey and then
sets expire on breachKey (see pipeline, breachKey, pipeline.expire, results,
breachCount) resets the TTL on every breach which keeps the window sliding; if
you want a fixed 1-hour window from the first breach change the expire call to
only set TTL when none exists (use the NX option) or set an absolute expiry via
EXPIREAT so the TTL is not refreshed on each incr; update the pipeline.expire
invocation to use the NX flag or switch to EXPIREAT to enforce a fixed window
instead of the current sliding behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d89ca6b-2457-409b-840b-06362cf9e2d3

📥 Commits

Reviewing files that changed from the base of the PR and between c557c24 and a903eb5.

📒 Files selected for processing (2)
  • lib/ratelimit.ts
  • lib/with-ratelimit.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/with-ratelimit.ts

Comment thread lib/ratelimit.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ratelimit.ts`:
- Around line 22-25: The current fail-open behavior in the rate limiter silently
bypasses limits when redisClient is falsy; update the logic in the rate limit
module (the redisClient check in lib/ratelimit.ts and any enclosing function
that returns the object with success/limit/remaining/reset/retryAfter) to avoid
silently disabling rate limiting in production: either throw an error or emit a
structured/critical log and fail startup when UPSTASH_REDIS_* credentials are
missing, or make the behavior configurable via an env flag (e.g.,
FAIL_OPEN_RATE_LIMIT=true) so production defaults to fail-closed; replace the
console.warn with a processLogger.error/throw and ensure any callers handle the
thrown error or environment flag accordingly.
- Around line 56-63: Replace the console.log call in the rate-limit breach
branch with a call to logToAudit using the same payload (severity: 'WARNING',
event: 'CONSISTENT_RATE_LIMIT_BREACH', target: key, message: ...); update the
call site in the function that checks breachCount (the block where breachCount
=== 3 references key) and ensure logToAudit is imported from the audit logging
module (add the import for logToAudit at the top of lib/ratelimit.ts if missing)
so the event is persisted to the AuditLog system instead of printed to stdout.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83ef5d6d-ca01-4b3e-a58f-4a6b69b61602

📥 Commits

Reviewing files that changed from the base of the PR and between a903eb5 and 0f3d9d9.

📒 Files selected for processing (1)
  • lib/ratelimit.ts

Comment thread lib/ratelimit.ts
Comment thread lib/ratelimit.ts
@MrImmortal09

Copy link
Copy Markdown
Collaborator

@coderabbitai full-review

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

@MrImmortal09, I'll perform a full review of PR #45 now. I'll evaluate the rate limiting implementation across all changed files — lib/ratelimit.ts, lib/with-ratelimit.ts, .env.example, and package.json — and assess whether it meets the acceptance criteria from issue #29.

✅ Actions performed

Full review triggered.

@vercel

vercel Bot commented Apr 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
alumni Ready Ready Preview, Comment Apr 12, 2026 0:44am

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
.env.example (1)

11-15: ⚠️ Potential issue | 🟡 Minor

Fix the Mailgun block so dotenv-linter stays clean.

EMAIL_FROM is still quoted and still ordered after the Mailgun keys, so the same QuoteCharacter / UnorderedKey warnings remain in this block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 11 - 15, The Mailgun env block violates
dotenv-linter rules: remove quotes from EMAIL_FROM and reorder keys so
EMAIL_FROM is placed before the Mailgun-specific keys; specifically ensure
EMAIL_FROM is unquoted (EMAIL_FROM=no-reply@yourdomain.com) and that
MAILGUN_API_KEY, MAILGUN_DOMAIN, and optional MAILGUN_URL follow the correct
ordering (MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_URL) to clear the
QuoteCharacter and UnorderedKey warnings for the symbols EMAIL_FROM,
MAILGUN_API_KEY, MAILGUN_DOMAIN, and MAILGUN_URL.
lib/ratelimit.ts (2)

21-25: ⚠️ Potential issue | 🟠 Major

Don’t silently disable rate limiting when Upstash credentials are missing.

This branch returns success: true, so every protected write endpoint becomes unlimited whenever the env vars are unset or misconfigured. For this feature, use an in-memory fallback or fail fast in production instead of bypassing enforcement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 21 - 25, The current limit function silently
bypasses enforcement when redisClient is missing; update limit(key: string,
config: RateLimitConfig) to: (a) detect missing redisClient and, depending on
environment, either use an in-memory fallback rate limiter (e.g., a
process-scoped Map with timestamps and counters keyed by the same key logic) to
compute and return { success, limit, remaining, reset, retryAfter } or (b) in
production fail fast by throwing an error so endpoints do not become unlimited;
ensure the in-memory fallback respects config.maxRequests and config.windowMs
and uses the same response shape, and keep the redisClient path unchanged so
callers of limit() (and the RateLimitConfig type) behave consistently.

56-62: ⚠️ Potential issue | 🟠 Major

Persist breach events to AuditLog and avoid raw identifiers.

console.log here does not meet the acceptance criterion to log consistent breaches to AuditLog, and target: key writes the route/IP identifier verbatim. Call logToAudit(...) instead and pass a pseudonymized fingerprint rather than the raw key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ratelimit.ts` around lines 56 - 62, Replace the console.log call that
fires when breachCount === 3 with a call to logToAudit(...) recording the
CONSISTENT_RATE_LIMIT_BREACH event to the AuditLog; do not pass the raw
key—compute a pseudonymized fingerprint for the key (e.g., via existing
createFingerprint or pseudonymizeIdentifier helper, or add a short SHA256/HMAC
wrapper) and pass that fingerprint as the target, include breachCount in the
payload, and ensure the event name matches "CONSISTENT_RATE_LIMIT_BREACH" so
AuditLog ingestion can correlate it.
lib/with-ratelimit.ts (1)

27-29: ⚠️ Potential issue | 🟠 Major

Normalize the client key before rate limiting.

x-forwarded-for can be a comma-separated chain, and the "127.0.0.1" fallback collapses unrelated callers into one bucket. Parse and validate the first forwarded IP, then avoid a shared fallback key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/with-ratelimit.ts` around lines 27 - 29, The current rate-limit key
creation in with-ratelimit.ts uses raw headers and a hardcoded "127.0.0.1"
fallback causing shared buckets; update the logic that builds ip and identifier
so it parses x-forwarded-for as a comma-separated list and uses the first
non-empty entry, validate it (e.g., simple IPv4/IPv6 format check), and only if
validation fails use a per-request unique fallback (e.g.,
req.headers.get("x-real-ip") or the request's remote address) rather than the
static "127.0.0.1"; change the variables ip and identifier to reflect this
normalized/validated client key so identifier =
`${req.nextUrl.pathname}:${normalizedIp}`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/with-ratelimit.ts`:
- Around line 23-45: The withRateLimit wrapper is implemented (function
withRateLimit and its use of limit and NextResponse.json) but the write-flow API
endpoints (e.g., sign-up, contact submission, job posting, avatar upload)
referenced by Issue `#29` are missing; create API route handlers for each form
action (register, contact, job posting, avatar upload) that accept NextRequest,
implement the necessary validation and persistence logic, and wrap each handler
with withRateLimit(...) so the limiter is enforced; ensure each handler returns
proper NextResponse JSON errors and includes Retry-After headers on 429 by using
the same pattern as in withRateLimit when limit returns success=false.

---

Duplicate comments:
In @.env.example:
- Around line 11-15: The Mailgun env block violates dotenv-linter rules: remove
quotes from EMAIL_FROM and reorder keys so EMAIL_FROM is placed before the
Mailgun-specific keys; specifically ensure EMAIL_FROM is unquoted
(EMAIL_FROM=no-reply@yourdomain.com) and that MAILGUN_API_KEY, MAILGUN_DOMAIN,
and optional MAILGUN_URL follow the correct ordering (MAILGUN_API_KEY,
MAILGUN_DOMAIN, MAILGUN_URL) to clear the QuoteCharacter and UnorderedKey
warnings for the symbols EMAIL_FROM, MAILGUN_API_KEY, MAILGUN_DOMAIN, and
MAILGUN_URL.

In `@lib/ratelimit.ts`:
- Around line 21-25: The current limit function silently bypasses enforcement
when redisClient is missing; update limit(key: string, config: RateLimitConfig)
to: (a) detect missing redisClient and, depending on environment, either use an
in-memory fallback rate limiter (e.g., a process-scoped Map with timestamps and
counters keyed by the same key logic) to compute and return { success, limit,
remaining, reset, retryAfter } or (b) in production fail fast by throwing an
error so endpoints do not become unlimited; ensure the in-memory fallback
respects config.maxRequests and config.windowMs and uses the same response
shape, and keep the redisClient path unchanged so callers of limit() (and the
RateLimitConfig type) behave consistently.
- Around line 56-62: Replace the console.log call that fires when breachCount
=== 3 with a call to logToAudit(...) recording the CONSISTENT_RATE_LIMIT_BREACH
event to the AuditLog; do not pass the raw key—compute a pseudonymized
fingerprint for the key (e.g., via existing createFingerprint or
pseudonymizeIdentifier helper, or add a short SHA256/HMAC wrapper) and pass that
fingerprint as the target, include breachCount in the payload, and ensure the
event name matches "CONSISTENT_RATE_LIMIT_BREACH" so AuditLog ingestion can
correlate it.

In `@lib/with-ratelimit.ts`:
- Around line 27-29: The current rate-limit key creation in with-ratelimit.ts
uses raw headers and a hardcoded "127.0.0.1" fallback causing shared buckets;
update the logic that builds ip and identifier so it parses x-forwarded-for as a
comma-separated list and uses the first non-empty entry, validate it (e.g.,
simple IPv4/IPv6 format check), and only if validation fails use a per-request
unique fallback (e.g., req.headers.get("x-real-ip") or the request's remote
address) rather than the static "127.0.0.1"; change the variables ip and
identifier to reflect this normalized/validated client key so identifier =
`${req.nextUrl.pathname}:${normalizedIp}`.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30c4f685-f47f-420c-8b85-2d851daa35e6

📥 Commits

Reviewing files that changed from the base of the PR and between c9b3504 and 0f3d9d9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .env.example
  • lib/ratelimit.ts
  • lib/with-ratelimit.ts
  • package.json

Comment thread lib/with-ratelimit.ts
@MrImmortal09 MrImmortal09 merged commit e62a73d into iiitl:main Apr 12, 2026
4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 12, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rate limiting middleware

3 participants