1
Fork 0
blog-python-mongo/app/main.py
Leonardo Devai 81b0a5ad14 Harmonize the blog engine contract across all eight backends
published_at is now the first-publish time (null for drafts, kept across edits
and unpublish/republish), slugs collide to -2/-3 and regenerate on retitle,
every error is {"error"} with 400/401/403/404/405/409 pinned, timestamps are
RFC 3339 UTC. GUIDELINES pins the contract; each backend passes the same 59-check
end-to-end script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
2026-09-27 21:25:37 +02:00

40 lines
1.4 KiB
Python

import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
from . import auth, db, posts
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.client, app.state.db = await db.connect(os.environ["MONGO_URL"], os.environ["MONGO_DB"])
yield
await app.state.client.close()
app = FastAPI(title="blog", lifespan=lifespan)
app.include_router(auth.router)
app.include_router(posts.router)
@app.get("/health", response_class=PlainTextResponse)
async def health() -> str:
return "ok"
# Every error leaves as {"error": "message"}, including framework-raised ones.
@app.exception_handler(StarletteHTTPException)
async def http_error(_: Request, exc: StarletteHTTPException) -> JSONResponse:
return JSONResponse({"error": str(exc.detail)}, status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_error(_: Request, exc: RequestValidationError) -> JSONResponse:
first = exc.errors()[0]
field = ".".join(str(part) for part in first["loc"] if part != "body")
message = f"{field}: {first['msg']}" if field else first["msg"]
return JSONResponse({"error": message}, status_code=400)