|
5 | 5 | import time |
6 | 6 | import structlog |
7 | 7 | from contextlib import asynccontextmanager |
8 | | -from fastapi import FastAPI, Request |
| 8 | +from fastapi import FastAPI, Request, Response |
9 | 9 | from fastapi.middleware.cors import CORSMiddleware |
10 | 10 | from fastapi.responses import JSONResponse |
11 | 11 | from slowapi import Limiter |
12 | 12 | from slowapi.errors import RateLimitExceeded |
13 | 13 | from slowapi.middleware import SlowAPIMiddleware |
14 | 14 | from slowapi.util import get_remote_address |
15 | | -from routers import ( |
16 | | - pinned, |
17 | | - query, |
18 | | - export, |
19 | | - history, |
20 | | - webhooks, |
21 | | - payments, |
22 | | - messages, |
23 | | - analytics, |
24 | | - legal, |
25 | | - seo, |
26 | | - emails, |
27 | | - shares, |
28 | | -) |
| 15 | +from routers import ( |
| 16 | + pinned, |
| 17 | + query, |
| 18 | + export, |
| 19 | + history, |
| 20 | + webhooks, |
| 21 | + payments, |
| 22 | + messages, |
| 23 | + analytics, |
| 24 | + legal, |
| 25 | + seo, |
| 26 | + emails, |
| 27 | + shares, |
| 28 | +) |
29 | 29 | from auth import get_supabase_admin |
30 | 30 | from services.cache import close_redis, get_redis |
31 | 31 | from services.inference import close_client |
@@ -334,11 +334,11 @@ async def llm_error_handler(request: Request, exc: LLMError): |
334 | 334 | app.include_router(history.router, prefix="/api") |
335 | 335 | app.include_router(analytics.router, prefix="/api") |
336 | 336 | app.include_router(legal.router, prefix="/api") |
337 | | -app.include_router(seo.router) |
338 | | -app.include_router(emails.router, prefix="/api") |
339 | | -app.include_router(webhooks.router) # No prefix - webhooks use full path |
340 | | -app.include_router(payments.router, prefix="/api") |
341 | | -app.include_router(shares.router, prefix="/api") |
| 337 | +app.include_router(seo.router) |
| 338 | +app.include_router(emails.router, prefix="/api") |
| 339 | +app.include_router(webhooks.router) # No prefix - webhooks use full path |
| 340 | +app.include_router(payments.router, prefix="/api") |
| 341 | +app.include_router(shares.router, prefix="/api") |
342 | 342 |
|
343 | 343 |
|
344 | 344 | @app.get("/api/health", tags=["health"]) |
@@ -378,6 +378,7 @@ async def check_rate_limit() -> dict[str, str]: |
378 | 378 | log_fn("rate_limit_health_probe_failed", severity="error" if is_prod else "warning", error=str(exc)) |
379 | 379 | return {"status": status} |
380 | 380 |
|
| 381 | + |
381 | 382 | async def check_db() -> dict[str, str]: |
382 | 383 | try: |
383 | 384 | if not settings.supabase_url or not settings.supabase_secret_key: |
@@ -416,6 +417,36 @@ async def check_db() -> dict[str, str]: |
416 | 417 | } |
417 | 418 |
|
418 | 419 |
|
| 420 | +@app.get("/api/keep-alive", tags=["health"]) |
| 421 | +async def keep_alive(request: Request): |
| 422 | + """Minimal keep-alive endpoint for Supabase and Vercel cold-start mitigation.""" |
| 423 | + cron_secret = os.getenv("CRON_SECRET") |
| 424 | + if cron_secret: |
| 425 | + provided = request.query_params.get("key") |
| 426 | + if not provided or provided != cron_secret: |
| 427 | + return JSONResponse(status_code=401, content={"error": "unauthorized"}) |
| 428 | + |
| 429 | + supabase = get_supabase_admin() |
| 430 | + if not supabase: |
| 431 | + return JSONResponse(status_code=503, content={"error": "supabase_unavailable"}) |
| 432 | + |
| 433 | + await asyncio.to_thread( |
| 434 | + lambda: supabase.table("conversations").select("id").limit(1).execute() |
| 435 | + ) |
| 436 | + return JSONResponse(status_code=200, content={"ok": True}) |
| 437 | + |
| 438 | + |
| 439 | +@app.head("/api/keep-alive", tags=["health"]) |
| 440 | +async def keep_alive_head(request: Request): |
| 441 | + """HEAD variant that avoids Supabase calls.""" |
| 442 | + cron_secret = os.getenv("CRON_SECRET") |
| 443 | + if cron_secret: |
| 444 | + provided = request.query_params.get("key") |
| 445 | + if not provided or provided != cron_secret: |
| 446 | + return Response(status_code=401) |
| 447 | + return Response(status_code=200) |
| 448 | + |
| 449 | + |
419 | 450 | # Catch-all route for debugging (should be last) |
420 | 451 | @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) |
421 | 452 | async def catch_all(request: Request, path: str): |
|
0 commit comments