From bc0b92bd4d3e8b280f821bd53b3d638dcdc5c14f Mon Sep 17 00:00:00 2001 From: Noah Holtware Date: Thu, 5 Nov 2026 18:22:03 -0600 Subject: [PATCH] fix: identify honestly to HN and retry with backoff on 419/429/5xx The spoofed Chrome User-Agent drew HTTP 419 from HN; an honest UA gets 200. Co-Authored-By: Claude Sonnet 5.5 --- src/favorites.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/favorites.ts b/src/favorites.ts index 2d8dd26..626528b 100644 --- a/src/favorites.ts +++ b/src/favorites.ts @@ -48,16 +48,30 @@ export function parseFavorites(html: string, limit = 10): Favorite[] { .slice(0, limit); } -export async function fetchFavoritesHtml(user: string): Promise { - const resp = await fetch(`${HN}/favorites?id=${encodeURIComponent(user)}`, { - headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - }, - }); - const html = await resp.text(); - if (!resp.ok) { - throw new Error(`HN responded ${resp.status}: ${html.slice(0, 300)}`); +const USER_AGENT = + "hackernews-favs-cron/1.0 (personal homelab job; one request per week)"; +const RETRYABLE = new Set([419, 429, 500, 502, 503, 504]); + +/** + * Fetch the favorites page, identifying ourselves honestly. Retries a few + * times with exponential backoff on rate-limit/transient statuses, then fails + * loudly (the CronJob's backoffLimit takes over). + */ +export async function fetchFavoritesHtml( + user: string, + attempts = 4, + baseDelayMs = 5000, +): Promise { + const url = `${HN}/favorites?id=${encodeURIComponent(user)}`; + for (let attempt = 1; ; attempt++) { + const resp = await fetch(url, { headers: { "User-Agent": USER_AGENT } }); + const html = await resp.text(); + if (resp.ok) return html; + if (!RETRYABLE.has(resp.status) || attempt >= attempts) { + throw new Error( + `HN responded ${resp.status} after ${attempt} attempt(s): ${html.slice(0, 300)}`, + ); + } + await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** (attempt - 1))); } - return html; }