refactor(feed): single RSS client closes search timeout gap

fetchFeedXml owns headers + 20s timeout; both hand-rolled fetches in
feed.ts (fetchEpisodes, load-more cold path) and searchByFeedUrl route
through it — direct-URL search hung indefinitely before.
This commit is contained in:
2026-09-03 08:33:19 -04:00
parent c2ec356a5f
commit 3e90f9e783
3 changed files with 85 additions and 322 deletions

31
src/utils/rss-client.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* RSS feed client — single owner of feed XML fetches: headers, timeout,
* and failure folding to null.
*/
/** Default per-feed fetch timeout (ms). */
export const FETCH_TIMEOUT_MS = 20_000;
/**
* Fetch a feed's raw XML. Identity encoding keeps the response raw; the
* Accept list matches what podcast servers send. Any failure (network,
* non-ok, timeout) resolves to null — callers must leave data untouched.
*/
export const fetchFeedXml = async (
url: string,
opts?: { timeoutMs?: number },
): Promise<string | null> => {
try {
const response = await fetch(url, {
headers: {
"Accept-Encoding": "identity",
Accept: "application/rss+xml, application/xml, text/xml, */*",
},
signal: AbortSignal.timeout(opts?.timeoutMs ?? FETCH_TIMEOUT_MS),
});
if (!response.ok) return null;
return await response.text();
} catch {
return null;
}
};

View File

@@ -1,5 +1,6 @@
import { searchSourceByType, searchEpisodesByType } from "./source-searcher";
import { parseRSSFeed } from "../api/rss-parser";
import { fetchFeedXml } from "./rss-client";
import { SourceType } from "../types/source";
import type { PodcastSource, SearchResult } from "../types/source";
@@ -81,15 +82,8 @@ export const searchByFeedUrl = async (
if (!FEED_URL_RE.test(trimmed)) return [];
try {
const response = await fetch(trimmed, {
headers: {
"Accept-Encoding": "identity",
Accept: "application/rss+xml, application/xml, text/xml, */*",
},
});
if (!response.ok) return [];
const xml = await response.text();
const xml = await fetchFeedXml(trimmed);
if (xml === null) return [];
const podcast = parseRSSFeed(xml, trimmed);
return [