Fix toast accept stale closure + harden backend error responses
Toast accept button captured a stale `respond` reference from the Sonner closure. Use respondRef pattern so clicks always dispatch through the current mutation. Backend respond endpoint now catches unhandled exceptions and returns proper JSON with detail field instead of plain-text 500s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6b59d61bf3
commit
2fb41e0cf4
@ -9,6 +9,7 @@ Security:
|
||||
- Audit logging for all connection events
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import date as date_type, datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Path, Query, Request
|
||||
@ -49,6 +50,7 @@ from app.services.connection import (
|
||||
from app.services.notification import create_notification
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────
|
||||
@ -355,6 +357,24 @@ async def respond_to_request(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Accept or reject a connection request. Atomic via UPDATE...WHERE status='pending'."""
|
||||
try:
|
||||
return await _respond_to_request_inner(body, request, background_tasks, request_id, db, current_user)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
# get_db middleware auto-rollbacks on unhandled exceptions
|
||||
logger.exception("Unhandled error in respond_to_request (request_id=%s, user=%s)", request_id, current_user.id)
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error while processing connection response (request {request_id})")
|
||||
|
||||
|
||||
async def _respond_to_request_inner(
|
||||
body: RespondRequest,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
request_id: int,
|
||||
db: AsyncSession,
|
||||
current_user: User,
|
||||
) -> RespondAcceptResponse | RespondRejectResponse:
|
||||
now = datetime.now()
|
||||
|
||||
# Atomic update — only succeeds if status is still 'pending' and receiver is current user
|
||||
|
||||
@ -16,6 +16,9 @@ export default function NotificationToaster() {
|
||||
const prevUnreadRef = useRef(0);
|
||||
// Track in-flight request IDs so repeated clicks are blocked
|
||||
const respondingRef = useRef<Set<number>>(new Set());
|
||||
// Always call the latest respond — Sonner toasts capture closures at creation time
|
||||
const respondRef = useRef(respond);
|
||||
respondRef.current = respond;
|
||||
|
||||
const handleConnectionRespond = useCallback(
|
||||
async (requestId: number, action: 'accept' | 'reject', toastId: string | number) => {
|
||||
@ -30,7 +33,7 @@ export default function NotificationToaster() {
|
||||
);
|
||||
|
||||
try {
|
||||
await respond({ requestId, action });
|
||||
await respondRef.current({ requestId, action });
|
||||
toast.dismiss(loadingId);
|
||||
toast.success(action === 'accept' ? 'Connection accepted' : 'Request declined');
|
||||
} catch (err) {
|
||||
@ -40,7 +43,7 @@ export default function NotificationToaster() {
|
||||
respondingRef.current.delete(requestId);
|
||||
}
|
||||
},
|
||||
[respond],
|
||||
[],
|
||||
);
|
||||
|
||||
// Track unread count changes to force-refetch the list
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user