From eaac93306f7b0ec07dbf527d062c5e7c59c9321e Mon Sep 17 00:00:00 2001 From: idoshamun Date: Sun, 5 Apr 2026 19:18:49 +0000 Subject: [PATCH 1/2] fix(shared): guard malformed normalized feed items --- packages/shared/src/components/Feed.spec.tsx | 43 +++++++++++++++++++ packages/shared/src/graphql/feed.spec.ts | 37 ++++++++++++++++ packages/shared/src/graphql/feed.ts | 44 +++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/Feed.spec.tsx b/packages/shared/src/components/Feed.spec.tsx index 08c8acc04a..10dee87a61 100644 --- a/packages/shared/src/components/Feed.spec.tsx +++ b/packages/shared/src/components/Feed.spec.tsx @@ -364,6 +364,49 @@ describe('Feed logged in', () => { expect(await screen.findAllByTestId('postItem')).not.toHaveLength(0); }); + it('should skip malformed normalized post items without crashing', async () => { + const warn = jest + .spyOn(console, 'warn') + .mockImplementation(() => undefined); + + renderComponent([ + { + request: { + query: ANONYMOUS_FEED_QUERY, + variables, + }, + result: { + data: { + page: { + pageInfo: defaultFeedPage.pageInfo, + edges: [ + { + node: { + itemType: 'post', + feedMeta: null, + }, + }, + { + node: { + itemType: 'post', + feedMeta: null, + post: defaultFeedPage.edges[0].node, + }, + }, + ], + }, + }, + }, + }, + ]); + + await waitForNock(); + expect(await screen.findAllByTestId('postItem')).toHaveLength(1); + expect(warn).toHaveBeenCalledWith( + 'Skipping malformed feed item type: post', + ); + }); + it('should send upvote mutation', async () => { let mutationCalled = false; renderComponent([ diff --git a/packages/shared/src/graphql/feed.spec.ts b/packages/shared/src/graphql/feed.spec.ts index 85a517959d..d25af73a79 100644 --- a/packages/shared/src/graphql/feed.spec.ts +++ b/packages/shared/src/graphql/feed.spec.ts @@ -100,4 +100,41 @@ describe('normalizeFeedPage', () => { expect(normalizeFeedPage(data)).toEqual(data); }); + + it('should warn and skip malformed normalized feed items', () => { + const warn = jest + .spyOn(console, 'warn') + .mockImplementation(() => undefined); + const malformedData = { + page: { + pageInfo: defaultFeedPage.pageInfo, + edges: [ + { + node: { + itemType: 'post' as const, + feedMeta: null, + }, + }, + { + node: { + itemType: 'post' as const, + feedMeta: null, + post: defaultFeedPage.edges[0].node, + }, + }, + ], + }, + } as Parameters[0]; + + const result = normalizeFeedPage(malformedData); + + expect(warn).toHaveBeenCalledWith( + 'Skipping malformed feed item type: post', + ); + expect(result.page.edges).toHaveLength(1); + expect(result.page.edges[0].node).toMatchObject({ + itemType: 'post', + post: defaultFeedPage.edges[0].node, + }); + }); }); diff --git a/packages/shared/src/graphql/feed.ts b/packages/shared/src/graphql/feed.ts index 940540a1f2..546c25f724 100644 --- a/packages/shared/src/graphql/feed.ts +++ b/packages/shared/src/graphql/feed.ts @@ -80,6 +80,11 @@ const warnUnsupportedFeedItem = (itemType: string): void => { console.warn(`Skipping unsupported feed item type: ${itemType}`); }; +const warnMalformedFeedItem = (itemType: string): void => { + // eslint-disable-next-line no-console + console.warn(`Skipping malformed feed item type: ${itemType}`); +}; + const isFeedV2Typename = ( typename: FeedV2Item['__typename'] | Post['__typename'], ): typename is FeedV2Item['__typename'] => @@ -113,7 +118,7 @@ export const getFeedApiItemPost = ( item: FeedApiItem | FeedV2Item | Post, ): Post | null => { if (isFeedApiPostItem(item)) { - return item.post; + return item.post ?? null; } if (isFeedV2PostItem(item)) { @@ -164,6 +169,26 @@ const normalizeFeedV2Edge = ( return null; }; +const normalizeFeedApiEdge = ( + edge: Connection['edges'][number], +): Connection['edges'][number] | null => { + const { node } = edge; + + if (node.itemType !== 'post') { + warnUnsupportedFeedItem(node.itemType); + + return null; + } + + if (!node.post) { + warnMalformedFeedItem(node.itemType); + + return null; + } + + return edge; +}; + export const normalizeFeedPage = ( data: FeedData | FeedItemData | FeedV2Data, ): FeedItemData => { @@ -179,7 +204,22 @@ export const normalizeFeedPage = ( } if (isFeedApiItem(firstNode)) { - return data as FeedItemData; + return { + page: { + ...data.page, + edges: (data as FeedItemData).page.edges.reduce< + Connection['edges'] + >((normalizedEdges, edge) => { + const normalizedEdge = normalizeFeedApiEdge(edge); + + if (normalizedEdge) { + normalizedEdges.push(normalizedEdge); + } + + return normalizedEdges; + }, []), + }, + }; } if (isFeedV2Item(firstNode)) { From 8c2be84928013b04213af0856f5240a50b05d3a0 Mon Sep 17 00:00:00 2001 From: idoshamun Date: Sun, 5 Apr 2026 19:21:59 +0000 Subject: [PATCH 2/2] refactor(shared): simplify malformed feed item guard --- packages/shared/src/components/Feed.spec.tsx | 2 +- packages/shared/src/graphql/feed.spec.ts | 37 ----------------- packages/shared/src/graphql/feed.ts | 42 +------------------- packages/shared/src/hooks/useFeed.ts | 9 +++++ 4 files changed, 11 insertions(+), 79 deletions(-) diff --git a/packages/shared/src/components/Feed.spec.tsx b/packages/shared/src/components/Feed.spec.tsx index 10dee87a61..44bb5a32ba 100644 --- a/packages/shared/src/components/Feed.spec.tsx +++ b/packages/shared/src/components/Feed.spec.tsx @@ -403,7 +403,7 @@ describe('Feed logged in', () => { await waitForNock(); expect(await screen.findAllByTestId('postItem')).toHaveLength(1); expect(warn).toHaveBeenCalledWith( - 'Skipping malformed feed item type: post', + 'Skipping malformed normalized feed item type: post', ); }); diff --git a/packages/shared/src/graphql/feed.spec.ts b/packages/shared/src/graphql/feed.spec.ts index d25af73a79..85a517959d 100644 --- a/packages/shared/src/graphql/feed.spec.ts +++ b/packages/shared/src/graphql/feed.spec.ts @@ -100,41 +100,4 @@ describe('normalizeFeedPage', () => { expect(normalizeFeedPage(data)).toEqual(data); }); - - it('should warn and skip malformed normalized feed items', () => { - const warn = jest - .spyOn(console, 'warn') - .mockImplementation(() => undefined); - const malformedData = { - page: { - pageInfo: defaultFeedPage.pageInfo, - edges: [ - { - node: { - itemType: 'post' as const, - feedMeta: null, - }, - }, - { - node: { - itemType: 'post' as const, - feedMeta: null, - post: defaultFeedPage.edges[0].node, - }, - }, - ], - }, - } as Parameters[0]; - - const result = normalizeFeedPage(malformedData); - - expect(warn).toHaveBeenCalledWith( - 'Skipping malformed feed item type: post', - ); - expect(result.page.edges).toHaveLength(1); - expect(result.page.edges[0].node).toMatchObject({ - itemType: 'post', - post: defaultFeedPage.edges[0].node, - }); - }); }); diff --git a/packages/shared/src/graphql/feed.ts b/packages/shared/src/graphql/feed.ts index 546c25f724..cbb30f0663 100644 --- a/packages/shared/src/graphql/feed.ts +++ b/packages/shared/src/graphql/feed.ts @@ -80,11 +80,6 @@ const warnUnsupportedFeedItem = (itemType: string): void => { console.warn(`Skipping unsupported feed item type: ${itemType}`); }; -const warnMalformedFeedItem = (itemType: string): void => { - // eslint-disable-next-line no-console - console.warn(`Skipping malformed feed item type: ${itemType}`); -}; - const isFeedV2Typename = ( typename: FeedV2Item['__typename'] | Post['__typename'], ): typename is FeedV2Item['__typename'] => @@ -169,26 +164,6 @@ const normalizeFeedV2Edge = ( return null; }; -const normalizeFeedApiEdge = ( - edge: Connection['edges'][number], -): Connection['edges'][number] | null => { - const { node } = edge; - - if (node.itemType !== 'post') { - warnUnsupportedFeedItem(node.itemType); - - return null; - } - - if (!node.post) { - warnMalformedFeedItem(node.itemType); - - return null; - } - - return edge; -}; - export const normalizeFeedPage = ( data: FeedData | FeedItemData | FeedV2Data, ): FeedItemData => { @@ -204,22 +179,7 @@ export const normalizeFeedPage = ( } if (isFeedApiItem(firstNode)) { - return { - page: { - ...data.page, - edges: (data as FeedItemData).page.edges.reduce< - Connection['edges'] - >((normalizedEdges, edge) => { - const normalizedEdge = normalizeFeedApiEdge(edge); - - if (normalizedEdge) { - normalizedEdges.push(normalizedEdge); - } - - return normalizedEdges; - }, []), - }, - }; + return data as FeedItemData; } if (isFeedV2Item(firstNode)) { diff --git a/packages/shared/src/hooks/useFeed.ts b/packages/shared/src/hooks/useFeed.ts index 1b25456204..cc14f563c8 100644 --- a/packages/shared/src/hooks/useFeed.ts +++ b/packages/shared/src/hooks/useFeed.ts @@ -103,6 +103,15 @@ const getPostFeedItemOrWarn = ( item: FeedItemData['page']['edges'][number]['node'], ): Post | null => { if (item.itemType === 'post') { + if (!item.post) { + // eslint-disable-next-line no-console + console.warn( + `Skipping malformed normalized feed item type: ${item.itemType}`, + ); + + return null; + } + return item.post; }