Skip to content

fix(api): rate limit the billed places and weather tile routes - #544

Open
silvertakana wants to merge 1 commit into
mainfrom
fix/billed-key-rate-limits
Open

silvertakana wants to merge 1 commit into
mainfrom
fix/billed-key-rate-limits

Conversation

@silvertakana

Copy link
Copy Markdown
Owner

Why

Two routes hold billed third-party keys server-side and had no throttle at all:

  • /api/places/search and /api/places/details proxy Google Places (billed per request), and search is publicly reachable — so an anonymous client could run up the bill with no limit in the way.
  • /api/weather/tile/... proxies billed raster tiles, and one viewport pulls dozens of tiles per pan or zoom.

What

Two limiters added to src/lib/rateLimiters.ts, following the existing pattern:

Limiter Budget Applies to
placesLimiter 30 requests / 60 s / IP places/search, places/details
weatherTileLimiter 240 requests / 60 s / IP weather/tile/[z]/[x]/[y]

Each route runs the check as its first statement, before parsing and before any upstream call, and reuses the 429 shape the already-limited routes serve — Retry-After plus {"error":"Too many requests"} — rather than inventing a second one.

The weather budget is deliberately generous: a map viewport pulls many tiles at once, and 240/min still bounds a single client to well under the upstream per-minute allowance.

Tests

New src/lib/rateLimiters.test.ts (9 tests): under-limit passes, over-limit is refused, the counter is per-IP, and the 429 wire shape is asserted. Expected budgets are literals, so a changed limit cannot keep the suite green.

  • Targeted: 2 files, 21 tests pass.
  • src/lib/ as a whole: 60 files, 767 tests pass.
  • tsc --noEmit and eslint clean on every touched file.
  • Mutation-checked: setting either shipped budget to 1 fails the suite naming the shipped value (expected 'allowed=1 blocked=30' to be 'allowed=30 blocked=1'), then restored and re-run green.

Independent of the plugin-data routing work in flight elsewhere.

Both routes hold billed upstream keys server-side and had no throttle, and
place search is publicly reachable, so one anonymous client could run up
the bill or exhaust the quota.

- placesLimiter: 30 req/min/IP for places/search and places/details
- weatherTileLimiter: 240 req/min/IP for the raster tile route
- the check runs first in GET, before parsing or any upstream call, and
  reuses the existing 429 shape
- add limiter unit tests with literal budgets
@github-actions github-actions Bot added the size:S label Oct 4, 2026
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Left for a person rather than auto-merged: not a Dependabot pull request. Patch and minor updates merge themselves once every check has completed green; this one is listed in the weekly dependency digest.

@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
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.

1 participant