From e5ead50a3d0f74b7e0cacf6e3f8a61ba25ff1d51 Mon Sep 17 00:00:00 2001 From: vimzh Date: Sun, 24 May 2026 11:46:19 +0530 Subject: [PATCH 1/3] fix(tweet-preview): guard JSON.parse to avoid crashing on malformed tweet data TweetPreview was passing the result of `JSON.parse(data)` straight into `enrichTweet` whenever `data` was a string. If a document's stored tweet metadata was ever a non-JSON string, truncated JSON, or `"null"` / `"123"`, the unguarded parse threw and crashed the React tree rendering the document modal or memory card. Extract the parse into a `parseTweetData` helper that returns `null` when the input is missing, fails to parse, or doesn't resolve to an object. TweetPreview now renders nothing in those cases instead of crashing. Also widen the prop type from `Tweet` to `Tweet | string` to reflect the runtime input both call sites already pass. --- .../components/document-cards/tweet-preview.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index 807ba5b9a..894d4d12c 100644 --- a/apps/web/components/document-cards/tweet-preview.tsx +++ b/apps/web/components/document-cards/tweet-preview.tsx @@ -117,14 +117,26 @@ function CustomTweetMedia({ ) } +function parseTweetData(data: Tweet | string): Tweet | null { + if (!data) return null + if (typeof data !== "string") return data + try { + const parsed = JSON.parse(data) + return parsed && typeof parsed === "object" ? (parsed as Tweet) : null + } catch { + return null + } +} + export function TweetPreview({ data, noBgColor, }: { - data: Tweet + data: Tweet | string noBgColor?: boolean }) { - const parsedTweet = typeof data === "string" ? JSON.parse(data) : data + const parsedTweet = parseTweetData(data) + if (!parsedTweet) return null const tweet = enrichTweet(parsedTweet) return ( From 0944e5fb33bbd5be645b884a50147470cc18e239 Mon Sep 17 00:00:00 2001 From: vimzh Date: Sun, 24 May 2026 11:54:53 +0530 Subject: [PATCH 2/3] fix(tweet-preview): reject array inputs in parseTweetData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous guard accepted any non-null `typeof === "object"`, which means `JSON.parse("[]")` or `JSON.parse("[1,2,3]")` slipped through and reached `enrichTweet` — exactly the class of crash the helper was added to prevent. Add an `Array.isArray` check so only plain objects are passed downstream. --- apps/web/components/document-cards/tweet-preview.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index 894d4d12c..984b469c6 100644 --- a/apps/web/components/document-cards/tweet-preview.tsx +++ b/apps/web/components/document-cards/tweet-preview.tsx @@ -122,7 +122,9 @@ function parseTweetData(data: Tweet | string): Tweet | null { if (typeof data !== "string") return data try { const parsed = JSON.parse(data) - return parsed && typeof parsed === "object" ? (parsed as Tweet) : null + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Tweet) + : null } catch { return null } From 3fe7eb83d190d4988771f62e7b1623d5f6dbe070 Mon Sep 17 00:00:00 2001 From: vimzh Date: Sun, 24 May 2026 12:44:28 +0530 Subject: [PATCH 3/3] fix(tweet-preview): shape-check, fallback UI, memoize, and warn on bad data Copilot review flagged three things: 1. Casting an arbitrary parsed object to Tweet without validating shape can still crash inside enrichTweet. Added an isTweetLike type guard that requires the 'user' field, which both CustomTweetHeader and the downstream enrichTweet expect. 2. Returning null on bad data silently hid the failure. Now render a small 'Tweet preview unavailable' fallback (matching the style of the existing fallback in document-modal/content/tweet.tsx) and emit a console.warn so failures show up during debugging. 3. JSON.parse was running on every render. Wrapped in useMemo keyed by the data prop so reparsing only happens when the input changes. --- .../document-cards/tweet-preview.tsx | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/web/components/document-cards/tweet-preview.tsx b/apps/web/components/document-cards/tweet-preview.tsx index 984b469c6..77e90971c 100644 --- a/apps/web/components/document-cards/tweet-preview.tsx +++ b/apps/web/components/document-cards/tweet-preview.tsx @@ -1,6 +1,6 @@ "use client" -import { Suspense } from "react" +import { Suspense, useMemo } from "react" import type { Tweet } from "react-tweet/api" import { TweetBody, enrichTweet, TweetSkeleton } from "react-tweet" import { cn } from "@lib/utils" @@ -117,19 +117,43 @@ function CustomTweetMedia({ ) } +function isTweetLike(value: unknown): value is Tweet { + return ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + "user" in value + ) +} + function parseTweetData(data: Tweet | string): Tweet | null { if (!data) return null - if (typeof data !== "string") return data + if (typeof data !== "string") return isTweetLike(data) ? data : null try { - const parsed = JSON.parse(data) - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Tweet) - : null - } catch { + const parsed: unknown = JSON.parse(data) + if (isTweetLike(parsed)) return parsed + console.warn("TweetPreview: parsed value did not match Tweet shape") + return null + } catch (error) { + console.warn("TweetPreview: failed to parse tweet data", error) return null } } +function TweetPreviewFallback({ noBgColor }: { noBgColor?: boolean }) { + return ( +
+ Tweet preview unavailable +
+ ) +} + export function TweetPreview({ data, noBgColor, @@ -137,8 +161,8 @@ export function TweetPreview({ data: Tweet | string noBgColor?: boolean }) { - const parsedTweet = parseTweetData(data) - if (!parsedTweet) return null + const parsedTweet = useMemo(() => parseTweetData(data), [data]) + if (!parsedTweet) return const tweet = enrichTweet(parsedTweet) return (