diff --git a/src/lib/microcosm/feed.ts b/src/lib/microcosm/feed.ts index c3f356d..88d714a 100644 --- a/src/lib/microcosm/feed.ts +++ b/src/lib/microcosm/feed.ts @@ -149,13 +149,23 @@ async function resolveFeedGenDid( return (rec?.value as {did?: string} | undefined)?.did } +/** + * Feed generators we've seen reject a direct browser call (almost always a CORS + * block: the generator doesn't send Access-Control-Allow-Origin, so the browser + * refuses to read the response). We remember them to skip straight to the + * AppView next time instead of repeating a guaranteed-failing request. On native + * there is no CORS, so this set stays empty. + */ +const directFetchUnsupported = new Set() + /** * Fetch + hydrate one page of a custom (algorithmic) feed, with no AppView. * - * Uses Slingshot's batch hydration: it proxies the feed generator's - * getFeedSkeleton AND fetches every post record in ONE request, so we avoid N - * per-post record fetches (which made feeds load one-by-one). Only author - * identities are resolved afterward (cached + parallel). + * Calls the feed generator's getFeedSkeleton DIRECTLY (endpoint resolved via + * Slingshot), forwarding the viewer's service-auth token so personalized feeds + * work. The returned post uris are hydrated via Slingshot record reads. If the + * direct call can't be made (e.g. the generator lacks CORS headers in a + * browser), returns an empty page so the caller falls back to the AppView. */ export async function buildCustomFeed( feedUri: string, @@ -175,6 +185,9 @@ export async function buildCustomFeed( const feedGenDid = await resolveFeedGenDid(feedUri, signal) if (!feedGenDid) return {feed: []} + // Known to fail a direct browser call (CORS) - go straight to the AppView. + if (directFetchUnsupported.has(feedGenDid)) return {feed: []} + const authorization = opts.getAuthorization ? await opts.getAuthorization(feedGenDid).catch(() => undefined) : undefined @@ -182,6 +195,7 @@ export async function buildCustomFeed( // Call the feed generator's getFeedSkeleton DIRECTLY with the viewer's // service-auth token (no AppView, no Slingshot batch proxy). The generator // personalizes based on the token and returns an ordered list of post uris. + let corsBlocked = false const skeleton = await fetchFeedSkeletonDirect( { feedGenDid, @@ -191,7 +205,13 @@ export async function buildCustomFeed( authorization, }, signal, - ).catch(() => undefined) + ).catch((e: unknown) => { + // A browser CORS block surfaces as a TypeError ("Failed to fetch") with no + // status, distinct from an HTTP error. Remember it so we don't retry. + if (e instanceof TypeError) corsBlocked = true + return undefined + }) + if (corsBlocked) directFetchUnsupported.add(feedGenDid) if (!skeleton || skeleton.order.length === 0) return {feed: []} // Hydrate each post uri into a PostView (records + authors via Slingshot,