Skip to content

Commit 3bca780

Browse files
authored
ECHO-697 add CORS headers to public stats api (#459)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added cross-origin resource sharing (CORS) support to the public statistics API endpoint, enabling cross-origin requests and improving client compatibility. * **Bug Fixes** * Enhanced error handling for the statistics endpoint with improved timeout management, now returning appropriate service unavailability status during cache retrieval failures. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9f30e6f commit 3bca780

1 file changed

Lines changed: 20 additions & 5 deletions

File tree

echo/server/dembrane/api/stats.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from fastapi import Request, APIRouter, HTTPException
99
from pydantic import BaseModel
10+
from fastapi.responses import JSONResponse
1011

1112
from dembrane.directus import directus
1213
from dembrane.redis_async import get_redis_client
@@ -187,8 +188,22 @@ async def _release_lock() -> None:
187188
logger.warning("Lock release error: %s", e)
188189

189190

191+
_PUBLIC_CORS_HEADERS = {
192+
"Access-Control-Allow-Origin": "*",
193+
"Access-Control-Allow-Methods": "GET, OPTIONS",
194+
"Access-Control-Allow-Headers": "*",
195+
"Access-Control-Max-Age": "86400",
196+
}
197+
198+
199+
@StatsRouter.options("/")
200+
async def stats_preflight() -> JSONResponse:
201+
"""Handle CORS preflight for the public stats endpoint."""
202+
return JSONResponse(content=None, headers=_PUBLIC_CORS_HEADERS)
203+
204+
190205
@StatsRouter.get("/", response_model=StatsResponse)
191-
async def get_public_stats(request: Request) -> StatsResponse:
206+
async def get_public_stats(request: Request) -> JSONResponse:
192207
"""
193208
Public endpoint returning aggregate platform statistics.
194209
Rate-limited to 10 requests per IP per minute.
@@ -201,20 +216,20 @@ async def get_public_stats(request: Request) -> StatsResponse:
201216
# Check cache first
202217
cached = await _get_cached_stats()
203218
if cached is not None:
204-
return cached
219+
return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS)
205220

206221
# Cache miss — try to acquire lock to prevent stampede
207222
if await _acquire_lock():
208223
try:
209224
# Double-check cache (another request may have populated it)
210225
cached = await _get_cached_stats()
211226
if cached is not None:
212-
return cached
227+
return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS)
213228

214229
# Compute and cache fresh stats
215230
stats = await _compute_stats()
216231
await _set_cached_stats(stats)
217-
return stats
232+
return JSONResponse(content=stats.model_dump(), headers=_PUBLIC_CORS_HEADERS)
218233
finally:
219234
await _release_lock()
220235
else:
@@ -223,7 +238,7 @@ async def get_public_stats(request: Request) -> StatsResponse:
223238
await asyncio.sleep(0.5)
224239
cached = await _get_cached_stats()
225240
if cached is not None:
226-
return cached
241+
return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS)
227242

228243
# Lock holder likely failed — return 503 instead of stampeding Directus
229244
logger.warning("Stats computation timed out waiting for lock holder")

0 commit comments

Comments
 (0)