-
Notifications
You must be signed in to change notification settings - Fork 19
ECHO-697 add CORS headers to public stats api #459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| from fastapi import Request, APIRouter, HTTPException | ||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||
| from fastapi.responses import JSONResponse | ||||||||||||||||||
|
|
||||||||||||||||||
| from dembrane.directus import directus | ||||||||||||||||||
| from dembrane.redis_async import get_redis_client | ||||||||||||||||||
|
|
@@ -187,8 +188,22 @@ async def _release_lock() -> None: | |||||||||||||||||
| logger.warning("Lock release error: %s", e) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| _PUBLIC_CORS_HEADERS = { | ||||||||||||||||||
| "Access-Control-Allow-Origin": "*", | ||||||||||||||||||
| "Access-Control-Allow-Methods": "GET, OPTIONS", | ||||||||||||||||||
| "Access-Control-Allow-Headers": "*", | ||||||||||||||||||
| "Access-Control-Max-Age": "86400", | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @StatsRouter.options("/") | ||||||||||||||||||
| async def stats_preflight() -> JSONResponse: | ||||||||||||||||||
| """Handle CORS preflight for the public stats endpoint.""" | ||||||||||||||||||
| return JSONResponse(content=None, headers=_PUBLIC_CORS_HEADERS) | ||||||||||||||||||
|
Comment on lines
+199
to
+202
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Nit: preflight responses conventionally use
✨ Proposed tweak `@StatsRouter.options`("/")
async def stats_preflight() -> JSONResponse:
"""Handle CORS preflight for the public stats endpoint."""
- return JSONResponse(content=None, headers=_PUBLIC_CORS_HEADERS)
+ return JSONResponse(content=None, status_code=204, headers=_PUBLIC_CORS_HEADERS)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| @StatsRouter.get("/", response_model=StatsResponse) | ||||||||||||||||||
| async def get_public_stats(request: Request) -> StatsResponse: | ||||||||||||||||||
| async def get_public_stats(request: Request) -> JSONResponse: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Public endpoint returning aggregate platform statistics. | ||||||||||||||||||
| Rate-limited to 10 requests per IP per minute. | ||||||||||||||||||
|
|
@@ -201,20 +216,20 @@ async def get_public_stats(request: Request) -> StatsResponse: | |||||||||||||||||
| # Check cache first | ||||||||||||||||||
| cached = await _get_cached_stats() | ||||||||||||||||||
| if cached is not None: | ||||||||||||||||||
| return cached | ||||||||||||||||||
| return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Cache miss — try to acquire lock to prevent stampede | ||||||||||||||||||
| if await _acquire_lock(): | ||||||||||||||||||
| try: | ||||||||||||||||||
| # Double-check cache (another request may have populated it) | ||||||||||||||||||
| cached = await _get_cached_stats() | ||||||||||||||||||
| if cached is not None: | ||||||||||||||||||
| return cached | ||||||||||||||||||
| return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Compute and cache fresh stats | ||||||||||||||||||
| stats = await _compute_stats() | ||||||||||||||||||
| await _set_cached_stats(stats) | ||||||||||||||||||
| return stats | ||||||||||||||||||
| return JSONResponse(content=stats.model_dump(), headers=_PUBLIC_CORS_HEADERS) | ||||||||||||||||||
| finally: | ||||||||||||||||||
| await _release_lock() | ||||||||||||||||||
| else: | ||||||||||||||||||
|
|
@@ -223,7 +238,7 @@ async def get_public_stats(request: Request) -> StatsResponse: | |||||||||||||||||
| await asyncio.sleep(0.5) | ||||||||||||||||||
| cached = await _get_cached_stats() | ||||||||||||||||||
| if cached is not None: | ||||||||||||||||||
| return cached | ||||||||||||||||||
| return JSONResponse(content=cached.model_dump(), headers=_PUBLIC_CORS_HEADERS) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Lock holder likely failed — return 503 instead of stampeding Directus | ||||||||||||||||||
| logger.warning("Stats computation timed out waiting for lock holder") | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider using FastAPI's
CORSMiddlewareinstead of hand-rolling headers.Manual CORS is fragile — every response path (including error handlers) needs these headers or browsers will block the response. FastAPI's built-in
CORSMiddlewarehandles this automatically, including error responses and preflight. If you only need CORS on this single router, you can apply the middleware selectively or use a sub-application.That said, if there's a deliberate reason to keep it manual (e.g., no middleware access at this layer), then the immediate fix is ensuring all response paths include these headers — see the 503 comment below.
🤖 Prompt for AI Agents