Skip to content

fix(throttle): key the API rate limiter on the consumer, not the proxy IP - #280

Merged
roncodes merged 1 commit into
release/v1.6.66from
fix/per-consumer-throttle
Sep 29, 2026
Merged

roncodes merged 1 commit into
release/v1.6.66from
fix/per-consumer-throttle

Conversation

@roncodes

Copy link
Copy Markdown
Member

Problem

A single tenant sending order create/update requests through the public API put the whole platform into 429 Too many requests: other tenants' integrations, the driver and storefront apps, and console login.

Fleetbase\Http\Middleware\ThrottleRequests is the first middleware in fleetbase.api, ahead of AuthenticateOnceWithBasicAuth. When it runs, $request->user() is always null, so Laravel's resolveRequestSignature() falls back to route domain + $request->ip(). No route has a domain, and behind a load balancer the IP is the balancer's. Every caller therefore shared one 120/min bucket. The console's public routes (int/v1/auth/login, two-fa/check, settings/branding, lookup/*) produced the same cache key.

Reproduced locally through the full kernel: after 5 requests from tenant A (limit 5), both tenant B (different key, different client IP) and an anonymous console lookup got 429.

Fix

  • resolveRequestSignature() keys on the SHA-1 of the presented credential (Bearer API key, Sanctum token or basic auth). It needs no database lookup and doesn't depend on proxy configuration. It falls back to the authenticated user, then the IP, only when no credential is sent.
  • The key is scoped by the first path segment, so /v1 and /int never share a bucket.
  • Handler: the 429 response now keeps Retry-After and X-RateLimit-*. Previously they were dropped, so clients couldn't back off correctly.

Related PRs

  • The storefront limiter had the same bug and produced an identical cache key: fleetbase/storefront (fix/per-consumer-throttle)
  • The host app now trusts private-network proxies when resolving the client IP: fleetbase/fleetbase (fix/trusted-proxies)

Tests

  • New: two credentials behind one proxy IP get separate buckets; /int stays separate from /v1; the user/IP fallback ordering; the 429 keeps its headers.
  • MiddlewareContractsTest and ExceptionHandlerTest: 59 passed. composer test:lint is clean.

…y IP

ThrottleRequests runs before API authentication, so Laravel's default
signature always fell back to route domain + client IP. Behind a load
balancer that IP is the balancer's, and no route has a domain, so every
tenant, API key and console visitor shared one bucket: one busy
integration returned 429 to the whole platform.

Key the limiter on the hashed credential (API key, Sanctum token, basic
auth), falling back to the user and then the IP only when none is sent,
and scope it by first path segment so /v1 and /int stay separate.

Also keep Retry-After and X-RateLimit-* on the 429 response so throttled
clients know when to retry.
@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b82d921) to head (69df6f4).

Additional details and impacted files
@@             Coverage Diff             @@
##                main      #280   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity      7676      7679    +3     
===========================================
  Files            433       433           
  Lines          25009     25016    +7     
===========================================
+ Hits           25009     25016    +7     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@roncodes
roncodes changed the base branch from main to release/v1.6.66 September 29, 2026 08:27
@roncodes
roncodes merged commit 975ced0 into release/v1.6.66 Sep 29, 2026
7 checks passed
@roncodes
roncodes deleted the fix/per-consumer-throttle branch September 29, 2026 09:24
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.

1 participant