From 30e46c05431a5c40e803d25533c88c9fd9709a24 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 10 Jan 2026 07:02:54 -0500 Subject: [PATCH] fix: handle RSS guids with missing text content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some RSS feeds (e.g., Marketing Week) return guid elements with only XML attributes but no text content, resulting in objects like `{"$":{"isPermaLink":"false"}}` instead of strings. This caused "Cannot convert object to primitive value" errors. - Update FeedItem type to reflect optional `_` (text) and `$` (attrs) - Extract guid text content properly, falling back to link URL - Remove per-feed debug logs to reduce log volume - Only log RSS processing summary when there are new articles or errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .changeset/eighty-sloths-feel.md | 2 ++ server/src/addie/services/feed-fetcher.ts | 38 +++++++++++------------ server/src/db/industry-feeds-db.ts | 5 +-- 3 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 .changeset/eighty-sloths-feel.md diff --git a/.changeset/eighty-sloths-feel.md b/.changeset/eighty-sloths-feel.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/eighty-sloths-feel.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/src/addie/services/feed-fetcher.ts b/server/src/addie/services/feed-fetcher.ts index 709b02a6ed..f0a1811dc4 100644 --- a/server/src/addie/services/feed-fetcher.ts +++ b/server/src/addie/services/feed-fetcher.ts @@ -68,7 +68,7 @@ function validateRssContent(content: string, contentType: string): { valid: bool } interface FeedItem { - guid?: string | { _: string }; + guid?: string | { _?: string; $?: Record }; link?: string; title?: string; pubDate?: string; @@ -98,12 +98,9 @@ function isRssFetchable(feedUrl: string | null): feedUrl is string { async function fetchFeed(feed: IndustryFeed): Promise { // Skip email-only feeds (those with non-HTTP URLs like email://) if (!isRssFetchable(feed.feed_url)) { - logger.debug({ feedId: feed.id, name: feed.name, url: feed.feed_url }, 'Skipping non-HTTP feed'); return []; } - logger.debug({ feedId: feed.id, name: feed.name, url: feed.feed_url }, 'Fetching RSS feed'); - // Pre-fetch content to validate it's actually RSS/XML before parsing // This prevents cryptic XML parsing errors when sites return HTML const response = await fetch(feed.feed_url, { @@ -131,7 +128,6 @@ async function fetchFeed(feed: IndustryFeed): Promise { const parsed = await parser.parseString(content); if (!parsed.items || parsed.items.length === 0) { - logger.warn({ feedId: feed.id }, 'Feed returned no items'); return []; } @@ -144,11 +140,18 @@ async function fetchFeed(feed: IndustryFeed): Promise { } // Generate a stable GUID - // Handle RSS guids that may be objects with _ property (from XML attributes) + // Handle RSS guids that may be objects with _ property (text content) from XML parsing + // Some feeds have guid with only attributes ($) but no text content (_) const rawGuid = item.guid; - const guid = typeof rawGuid === 'object' && rawGuid !== null && '_' in rawGuid - ? rawGuid._ - : (rawGuid || item.link); + let guid: string; + if (typeof rawGuid === 'string') { + guid = rawGuid; + } else if (typeof rawGuid === 'object' && rawGuid !== null && '_' in rawGuid && rawGuid._) { + guid = rawGuid._; + } else { + // Fall back to link if guid is missing or empty object + guid = item.link; + } // Parse publication date let publishedAt: Date | undefined; @@ -183,7 +186,6 @@ async function fetchFeed(feed: IndustryFeed): Promise { }); } - logger.debug({ feedId: feed.id, articlesFound: articles.length }, 'Parsed RSS feed'); return articles; } @@ -198,12 +200,9 @@ export async function processFeedsToFetch(): Promise<{ const feeds = await getFeedsToFetch(); if (feeds.length === 0) { - logger.debug('No feeds need fetching'); return { feedsProcessed: 0, newPerspectives: 0, errors: 0 }; } - logger.debug({ feedCount: feeds.length }, 'Processing RSS feeds'); - let totalNewPerspectives = 0; let errorCount = 0; @@ -212,10 +211,8 @@ export async function processFeedsToFetch(): Promise<{ const articles = await fetchFeed(feed); if (articles.length > 0) { - // Create perspectives from RSS articles const created = await createRssPerspectivesBatch(articles); totalNewPerspectives += created; - logger.debug({ feedId: feed.id, created }, 'Created perspectives from RSS'); } await updateFeedStatus(feed.id, true); @@ -230,10 +227,13 @@ export async function processFeedsToFetch(): Promise<{ await new Promise(resolve => setTimeout(resolve, 500)); } - logger.info( - { feedsProcessed: feeds.length, newPerspectives: totalNewPerspectives, errors: errorCount }, - 'Completed RSS feed processing' - ); + // Only log when there's something notable + if (totalNewPerspectives > 0 || errorCount > 0) { + logger.info( + { feedsProcessed: feeds.length, newPerspectives: totalNewPerspectives, errors: errorCount }, + 'RSS feed processing complete' + ); + } return { feedsProcessed: feeds.length, diff --git a/server/src/db/industry-feeds-db.ts b/server/src/db/industry-feeds-db.ts index c22f15fc05..220003f71f 100644 --- a/server/src/db/industry-feeds-db.ts +++ b/server/src/db/industry-feeds-db.ts @@ -243,10 +243,7 @@ function generateSlug(title: string, guid: string): string { .replace(/^-|-$/g, '') .substring(0, 80); - // Ensure guid is a string (defensive against RSS parser quirks) - if (typeof guid !== 'string') { - logger.warn({ guid, title }, 'generateSlug received non-string guid'); - } + // Defensive: ensure guid is a string even if caller passes unexpected type const guidStr = String(guid || ''); // Add a hash of the guid to ensure uniqueness