Rate-limited API responses (429) carry no CORS headers — browser masks them as CORS errors #311

Closed
opened 2026-07-01 21:14:08 +00:00 by spikerj · 1 comment
Owner

Found by the E2E anonymous walk (epic #307) after moving the suite to the allowlisted https://localhost:4200 origin.

Symptom

GET /api/system/version from an allowlisted origin intermittently fails with No 'Access-Control-Allow-Origin' header is present, and the SPA's version badge falls back to OFFLINE. The origin is in the CORS allowlist — other endpoints work fine.

Root cause

Middleware order in SpikerSoft.Api/Extensions/WebApplicationExtensions.cs (ConfigureMiddleware): RedisRateLimitMiddleware and UseRateLimiter() are registered before UseCors(). When the limiter rejects a request (the version endpoint allows only 10/min/IP with QueueLimit = 0), the 429 short-circuits the pipeline before the CORS middleware runs, so the response has no Access-Control-Allow-Origin header and the browser reports a CORS failure instead of the real 429. Any rate-limited endpoint has the same behavior; the version endpoint is just the easiest to trip (the SPA calls it on every page load).

Fix

Move app.UseCors(...) above the two rate-limiting middlewares so preflights and 429s still carry CORS headers. Matches ASP.NET Core's recommended ordering (CORS before rate limiter).

Part of epic #307.

Found by the E2E anonymous walk (epic #307) after moving the suite to the allowlisted `https://localhost:4200` origin. ## Symptom `GET /api/system/version` from an allowlisted origin intermittently fails with `No 'Access-Control-Allow-Origin' header is present`, and the SPA's version badge falls back to OFFLINE. The origin *is* in the CORS allowlist — other endpoints work fine. ## Root cause Middleware order in `SpikerSoft.Api/Extensions/WebApplicationExtensions.cs` (`ConfigureMiddleware`): `RedisRateLimitMiddleware` and `UseRateLimiter()` are registered **before** `UseCors()`. When the limiter rejects a request (the version endpoint allows only 10/min/IP with `QueueLimit = 0`), the 429 short-circuits the pipeline before the CORS middleware runs, so the response has no `Access-Control-Allow-Origin` header and the browser reports a CORS failure instead of the real 429. Any rate-limited endpoint has the same behavior; the version endpoint is just the easiest to trip (the SPA calls it on every page load). ## Fix Move `app.UseCors(...)` above the two rate-limiting middlewares so preflights and 429s still carry CORS headers. Matches ASP.NET Core's recommended ordering (CORS before rate limiter). Part of epic #307.
spikerj added the bug label 2026-07-01 21:14:08 +00:00
Author
Owner

Resolved in spikersoft-backend PR #50 (merged to master). UseCors now runs before response caching and both rate limiters, so 429s carry Access-Control-Allow-Origin instead of masquerading as CORS failures. Closing.

Resolved in spikersoft-backend PR #50 (merged to `master`). `UseCors` now runs before response caching and both rate limiters, so 429s carry `Access-Control-Allow-Origin` instead of masquerading as CORS failures. Closing.
Sign in to join this conversation.