From b48a30cfc5ff8b2cf5810f75f895e04bef7e90e7 Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Fri, 6 Mar 2026 12:17:17 +0100 Subject: [PATCH] fix(reactions): handle empty response body on unlike and accept stale deletes (#194) apiFetch now safely handles empty response bodies by reading text first before parsing JSON, preventing the JSON.parse error on DELETE 204/200 responses. LikeButton's error handler now treats "Not Found" during unlike as a successful state transition instead of reverting, preventing count inflation from repeated like/unlike cycles. --- src/components/like-button.tsx | 17 ++++++++++++----- src/lib/api/client.ts | 10 ++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/like-button.tsx b/src/components/like-button.tsx index 0bdf6f9..9bfbb86 100644 --- a/src/components/like-button.tsx +++ b/src/components/like-button.tsx @@ -105,12 +105,19 @@ export function LikeButton({ reactionUriRef.current = result.uri } } catch (err) { - // Revert optimistic update - setLiked(wasLiked) - setCount(previousCount) - reactionUriRef.current = previousUri const message = err instanceof Error ? err.message : 'Failed to update reaction' - toast({ title: 'Error', description: message, variant: 'destructive' }) + const isNotFound = message === 'Not Found' || message.includes('not found') + + if (wasLiked && isNotFound) { + // Reaction was already deleted server-side -- accept the unliked state + reactionUriRef.current = null + } else { + // Revert optimistic update + setLiked(wasLiked) + setCount(previousCount) + reactionUriRef.current = previousUri + toast({ title: 'Error', description: message, variant: 'destructive' }) + } } finally { setPending(false) } diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 3e225ac..a4a799b 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -139,11 +139,17 @@ async function apiFetch(path: string, options: FetchOptions = {}): Promise await throwApiError(response) } - if (response.status === 204) { + const contentLength = response.headers.get('content-length') + if (response.status === 204 || contentLength === '0') { return undefined as T } - return response.json() as Promise + const text = await response.text() + if (!text) { + return undefined as T + } + + return JSON.parse(text) as T } function buildQuery(params: Record): string { -- 2.51.2