From 6f1f024e907b100d48b01cec1f029de2793af25f Mon Sep 17 00:00:00 2001 From: Florian Date: Sat, 31 Oct 2026 23:34:13 +0100 Subject: [PATCH] Added path to unsubscribe from RSS feeds and exception handler to propagate FeedErrors --- src/main.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index fba3760..0b6eb85 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ from contextlib import asynccontextmanager import uvicorn from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from requests.exceptions import RequestException @@ -63,6 +64,27 @@ api = FastAPI( async def feed_error_handler(request: Request, exc: FeedError): return JSONResponse(status_code=exc.status_code, content={"detail":str(exc)}) +@api.exception_handler(RequestValidationError) +async def validation_error_handler(request: Request, exc: RequestValidationError): + errors = exc.errors() + + # 1) If one of our own FeedErrors caused it, use its message as is + for err in errors: + original = (err.get("ctx") or {}).get("error") + if isinstance(original, FeedError): + return JSONResponse( + status_code=original.status_code, + content={"detail": str(original)}, + ) + + # 2) Anything else (missing field, bad enum value, bad header): flatten to one string + messages = [] + for err in errors: + field = ".".join(str(p) for p in err["loc"] if p != "body") + msg = err["msg"].removeprefix("Value error, ") + messages.append(f"{field}: {msg}" if field else msg) + return JSONResponse(status_code=422, content={"detail": "; ".join(messages)}) + @api.middleware("http") async def prometheus_middleware(request: Request, call_next): logger.debug(f"[FastAPI] Incoming request: {request.method} {request.url.path}") @@ -319,12 +341,57 @@ def add_feed( db.commit() except Exception as e: db.rollback() - logger.exception(f"[FastAPI] Failed to subscribe to user_id={user_id} to {url}") + logger.exception(f"[FastAPI] Failed to subscribe user_id={user_id} to {url}") raise HTTPException(status_code=500) from e logger.info(f"[FastAPI] user_id={user_id} subscribed to feed_id={feed_id}, url={url}") - return {"status":"ok","detail":"Subscribed to feed","feed_id":feed_id} + return {"status":"ok","detail":"Subscribed to feed","feed_url":url} +@user_router.post("/delete/feed") +def delete_feed( + user_id: UserId, + feed_entry: FeedEntry, + db = Depends(get_db), + ): + url = feed_entry.rss_feed_url + logger.info(f"[FastAPI] Unsubscribe request: RSS URL={url}, user_id={user_id}") + + try: + with db.cursor() as cursor: + cursor.execute( + "SELECT id FROM rss_feeds WHERE rss_feed = %s", + (url,), + ) + known = cursor.fetchone() + + if known is None: + db.rollback() + return {"status":"ok","detail":"Unsubscribed to feed","feed_url":url} + + feed_id = known[0] + cursor.execute( + "UPDATE rss_feed_subscriptions SET active = 0 " + "WHERE user_id = %s AND rss_feed_id = %s", + (user_id, feed_id), + ) + + cursor.execute( + "SELECT 1 from rss_feed_subscriptions " + "WHERE rss_feed_id = %s AND active = 1 LIMIT 1", + (feed_id,), + ) + if cursor.fetchone() is None: + cursor.execute( + "UPDATE rss_feeds SET active = 0 WHERE id = %s", + (feed_id,), + ) + + db.commit() + except Exception as e: + db.rollback() + logger.exception(f"[FastAPI] Failed to unsubscribe user_id={user_id} to {url}") + raise HTTPException(status_code=500) from e + return {"status":"ok","detail":"Unsubscribed to feed","feed_url":url} api.include_router(user_router) api.include_router(scheduler_router)