Skip to content

feat: add rate limiting to public API#11986

Merged
NeOMakinG merged 7 commits intodevelopfrom
feat/public-api-rate-limiting
Feb 27, 2026
Merged

feat: add rate limiting to public API#11986
NeOMakinG merged 7 commits intodevelopfrom
feat/public-api-rate-limiting

Conversation

@0xApotheosis
Copy link
Copy Markdown
Member

@0xApotheosis 0xApotheosis commented Feb 23, 2026

Description

Add tiered rate limiting to the public API using express-rate-limit to protect expensive swap endpoints from abuse. A single client hitting /v1/swap/rates fans out to 10+ external swapper protocols — without rate limiting, one abusive IP can exhaust external API quotas and degrade service for everyone.

Four tiers (all configurable via env vars):

Tier Endpoints Default Limit Env Var
Global All routes 300/min per IP RATE_LIMIT_GLOBAL_MAX
Data /v1/chains/*, /v1/assets/* 120/min per IP RATE_LIMIT_DATA_MAX
Swap Rates GET /v1/swap/rates 60/min per IP RATE_LIMIT_SWAP_RATES_MAX
Swap Quote POST /v1/swap/quote 45/min per IP RATE_LIMIT_SWAP_QUOTE_MAX

Widget compatibility: The swap widget polls rates every 15s (4 req/min), well under the 60/min limit. 429 responses return immediately so the next poll cycle succeeds normally.

Key details:

  • trust proxy set to 1 for correct IP detection behind Railway's reverse proxy
  • draft-7 standard headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) on every response
  • 429 responses return JSON matching the existing ErrorResponse format with code RATE_LIMIT_EXCEEDED
  • OpenAPI docs updated with 429 response schemas for all rate-limited endpoints

Bonus fix — resilient asset loading: getBaseAsset() throws via assertUnreachable for any chain not yet in KnownChainIds. When new chains are added to the asset generation pipeline before being added to KnownChainIds, this crashed the entire server on startup. Now assets.ts catches the error and includes unknown-chain assets with their existing data (without enrichment), so new chains no longer break the public API.

Issue (if applicable)

Closes #11676

Risk

Low risk. This only affects the public-api package (deployed independently on Railway). No changes to the main web app, swap widget, or any on-chain transaction logic. Rate limits use sensible defaults with generous headroom and are fully configurable via env vars without redeployment of code.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

None. This is server-side middleware only.

Testing

Engineering

  1. cd packages/public-api && yarn build:bundle && yarn start:prod
  2. Verify server starts normally
  3. Rapid-fire requests to /v1/swap/rates — confirm 429 after 60 requests within 1 minute:
    for i in $(seq 1 65); do curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3001/v1/swap/rates?sellAssetId=eip155:1/slip44:60&buyAssetId=eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&sellAmountCryptoBaseUnit=1000000000000000000"; done
  4. Verify 429 response body: { "error": "...", "code": "RATE_LIMIT_EXCEEDED" }
  5. Verify RateLimit-* headers on normal 200 responses:
    curl -i "http://localhost:3001/v1/chains"
  6. Verify env var override works: RATE_LIMIT_SWAP_RATES_MAX=5 yarn start:prod

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

No user-facing UI changes. Rate limiting is transparent to normal usage patterns. Abusive clients will receive a 429 JSON response with a Retry-After header.

Screenshots (if applicable)

N/A

Other

Also adds a gh pr edit --body workaround to CLAUDE.md — the GraphQL command fails on this repo due to deprecated Projects Classic fields, so we document the REST API alternative (gh api repos/.../pulls/<number> -X PATCH -F "body=@file").

Summary by CodeRabbit

  • New Features

    • Global and endpoint-specific API rate limiting (per-minute limits); rate-limited requests return 429 with Retry-After and rate-limit headers
    • Trust-proxy toggle via environment variable and example env vars to tune limits
  • Bug Fixes

    • Asset enrichment now handles errors gracefully to avoid processing interruptions
  • Documentation

    • PR workflow guidance added for editing PR descriptions via REST API

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0db7011 and 96bf268.

📒 Files selected for processing (1)
  • packages/public-api/src/docs/openapi.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/public-api/src/docs/openapi.ts

📝 Walkthrough

Walkthrough

Adds express-rate-limit and four configurable limiters, wires them into public API routes, adds 429 OpenAPI responses and a RateLimitErrorSchema, enables trust proxy, updates .env.example and package dependency, and guards asset enrichment with a try/catch.

Changes

Cohort / File(s) Summary
Rate Limiting Middleware
packages/public-api/src/middleware/rateLimit.ts
New module creating four express-rate-limit instances (globalLimiter, dataLimiter, swapRatesLimiter, swapQuoteLimiter), env-configurable maxima, shared 429 handler, and exported RateLimitErrorCode.
API Integration
packages/public-api/src/index.ts
Sets trust proxy from TRUST_PROXY, applies globalLimiter globally, and mounts dataLimiter/swapRatesLimiter/swapQuoteLimiter on chains/assets/swap routes.
OpenAPI docs
packages/public-api/src/docs/openapi.ts
Adds RateLimitErrorSchema and a shared 429 rateLimitResponse; attaches 429 responses to chains, assets, and swap endpoints.
Configuration & Dependencies
packages/public-api/.env.example, packages/public-api/package.json
Adds TRUST_PROXY=1 and commented RATE_LIMIT_* examples to .env.example; adds dependency express-rate-limit@^7.5.0.
Asset Enrichment Error Handling
packages/public-api/src/assets.ts
Wraps getBaseAsset(...) in try/catch; on error leaves asset unenriched and logs a warning.
Docs / Misc
CLAUDE.md
Small PR workflow note about editing PR descriptions added.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant App as Express App
  participant Lim as RateLimiter Middleware
  participant Handler as Route Handler
  participant Ext as External Service/DB

  Client->>App: HTTP request (e.g., GET /v1/assets)
  App->>Lim: pass through configured limiter
  Lim-->>App: allow or reject (429)
  alt allowed
    App->>Handler: request reaches handler
    Handler->>Ext: fetch/process data
    Ext-->>Handler: response
    Handler-->>App: send 200
    App-->>Client: 200 response
  else rejected
    Lim-->>Client: 429 with RateLimitErrorSchema and RateLimit-* headers
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I nibble logs and watch the tide,
Four gentle gates keep traffic wide,
A hop, a header, a counted beat,
When requests overflow, I softly bleat,
Carrots saved so services glide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add rate limiting to public API' clearly and concisely summarizes the main change, matching the PR's primary objective of implementing rate limiting.
Linked Issues check ✅ Passed The PR successfully implements rate limiting with four configurable tiers, proper error handling with standard headers, and OpenAPI documentation updates as required by issue #11676.
Out of Scope Changes check ✅ Passed All changes are directly related to rate limiting implementation. The bonus asset enrichment error handling and CLAUDE.md documentation update are minor supporting changes within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/public-api-rate-limiting

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.

Protect expensive swap endpoints from abuse with tiered rate limits:
- Global: 300 req/min per IP (all routes)
- Data: 120 req/min per IP (chains/assets)
- Swap Rates: 60 req/min per IP (GET /v1/swap/rates)
- Swap Quote: 45 req/min per IP (POST /v1/swap/quote)

All limits are configurable via environment variables and compatible
with the swap widget's 15-second polling interval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@0xApotheosis 0xApotheosis force-pushed the feat/public-api-rate-limiting branch from e14eb17 to f9c3149 Compare February 23, 2026 08:09
@0xApotheosis 0xApotheosis force-pushed the feat/public-api-rate-limiting branch from 35759c7 to 58827b3 Compare February 24, 2026 05:03
@0xApotheosis 0xApotheosis marked this pull request as ready for review February 24, 2026 05:06
@0xApotheosis 0xApotheosis requested a review from a team as a code owner February 24, 2026 05:06
gh pr edit --body fails on this repo due to deprecated Projects Classic
GraphQL fields. Document the REST API alternative.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

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

Inline comments:
In `@packages/public-api/src/assets.ts`:
- Around line 50-60: In the catch block that wraps the
getBaseAsset(asset.chainId) call (the try surrounding getBaseAsset,
enrichedAssetsById, assetId and asset), replace the silent swallow with a
structured log entry that records the caught error plus context (at minimum
assetId and asset.chainId and a short message like "failed to enrich asset with
base data"), then continue to set enrichedAssetsById[assetId] = asset as the
fallback; ensure you use the module's standard logger (e.g., processLogger or
logger) and include the error object/stack in the log metadata for debugging.

In `@packages/public-api/src/docs/openapi.ts`:
- Around line 209-210: Remove the newly added comment block containing the line
"// --- Shared Response Schemas ---" from the file; locate the standalone
comment token matching that exact text and delete it so the codebase conforms to
the "no code comments" guideline.

In `@packages/public-api/src/index.ts`:
- Line 23: The line app.set('trust proxy', 1) forces Express to trust
X-Forwarded-* headers; make this configurable via an environment variable (e.g.,
TRUST_PROXY) so non-proxy environments don't accept spoofed headers—read
process.env.TRUST_PROXY (treat empty/undefined as false), parse values like "1",
"true", or a numeric value accordingly, and call app.set('trust proxy',
parsedValue) instead of the hardcoded 1; update the initialization around
app.set('trust proxy', 1) to use this parsed env value so req.ip/rate-limiter
behavior is correct per deployment.

In `@packages/public-api/src/middleware/rateLimit.ts`:
- Around line 13-17: Create a string enum (e.g., export enum RateLimitErrorCode
{ RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED' }) and replace the hardcoded
'RATE_LIMIT_EXCEEDED' in the rateLimitHandler (const rateLimitHandler:
Options['handler']) with the enum value
(RateLimitErrorCode.RATE_LIMIT_EXCEEDED); also update the OpenAPI/schema usage
to reference the same enum (via z.nativeEnum(RateLimitErrorCode)) so both the
handler and the schema share the single enum constant.
- Around line 20-27: The createLimiter function is missing an explicit return
type; change its signature to return the RateLimitRequestHandler type (the type
returned by rateLimit()), e.g. declare createLimiter(envKey: string, defaultMax:
number): RateLimitRequestHandler => ..., and ensure you import
RateLimitRequestHandler from the rate-limit package (the same module that
provides rateLimit) so the returned value from rateLimit(...) is correctly
typed; keep the body using rateLimit({ windowMs: WINDOW_MS, max:
parseEnvInt(envKey, defaultMax), standardHeaders: 'draft-7', legacyHeaders:
false, handler: rateLimitHandler }) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 671b4fa and 58827b3.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • packages/public-api/.env.example
  • packages/public-api/package.json
  • packages/public-api/src/assets.ts
  • packages/public-api/src/docs/openapi.ts
  • packages/public-api/src/index.ts
  • packages/public-api/src/middleware/rateLimit.ts

Comment thread packages/public-api/src/assets.ts
Comment thread packages/public-api/src/docs/openapi.ts Outdated
Comment thread packages/public-api/src/index.ts Outdated
Comment thread packages/public-api/src/middleware/rateLimit.ts
Comment thread packages/public-api/src/middleware/rateLimit.ts Outdated
0xApotheosis and others added 3 commits February 25, 2026 10:56
- Log getBaseAsset failures with structured context instead of silently swallowing
- Make trust proxy configurable via TRUST_PROXY env var to prevent IP spoofing
- Centralize RATE_LIMIT_EXCEEDED in a string enum shared by handler and OpenAPI schema
- Add explicit RateLimitRequestHandler return type to createLimiter
- Remove section comment violating no-comments guideline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@NeOMakinG NeOMakinG left a comment

Choose a reason for hiding this comment

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

Image

My friend ralph is happy with this one

@NeOMakinG NeOMakinG enabled auto-merge (squash) February 27, 2026 11:19
@NeOMakinG NeOMakinG merged commit d601cf5 into develop Feb 27, 2026
4 checks passed
@NeOMakinG NeOMakinG deleted the feat/public-api-rate-limiting branch February 27, 2026 11:32
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 etc to mitigate endpoint abuse

2 participants