{"record":{"id":"6210d5140243ca7a","repo":"santifer/career-ops","slug":"hackernews-unexpected-item-response-for-thread","errorCode":null,"errorMessage":"hackernews: unexpected item response for thread ${threadId}","messagePattern":"hackernews: unexpected item response for thread (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/hackernews.mjs","lineNumber":161,"sourceCode":"\n/** @type {Provider} */\nexport default {\n  id: 'hackernews',\n\n  async fetch(entry, ctx) {\n    // Step 1: Find the latest \"Who is hiring?\" story id.\n    const searchData = await ctx.fetchJson(SEARCH_URL, { redirect: 'error' });\n    const threadId = resolveLatestThreadId(searchData);\n    if (!threadId) {\n      throw new Error('hackernews: could not find \"Ask HN: Who is hiring?\" thread in search results');\n    }\n\n    const threadHnUrl = `https://news.ycombinator.com/item?id=${threadId}`;\n\n    // Step 2: Fetch the thread item (children = top-level job comments).\n    const item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });\n    if (!item || typeof item !== 'object') {\n      throw new Error(`hackernews: unexpected item response for thread ${threadId}`);\n    }\n\n    const children = /** @type {any} */ (item).children;\n    if (!Array.isArray(children)) return [];\n\n    // Step 3: Parse each comment.\n    const jobs = [];\n    for (const child of children) {\n      // Skip deleted / dead / empty comments.\n      if (!child || child.deleted || child.dead) continue;\n      const text = typeof child.text === 'string' ? child.text : '';\n      if (!text.trim()) continue;\n\n      const parsed = parseHnComment(text, threadHnUrl);\n      if (!parsed) continue;\n\n      jobs.push({\n        title: parsed.title,","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/hackernews.mjs#L143-L179","documentation":"Thrown by hackernews fetch() when the Algolia items API returns a response for the resolved thread id that is null or not a plain object. A valid thread item is an object with a children[] array; anything else means the item endpoint returned an unexpected payload for that threadId.","triggerScenarios":"The threadId resolved from step 1 points at a story that has since been deleted/dead on HN (Algolia returns null for dead items); a transient Algolia 200-with-null; the items endpoint rate-limited and returned a non-object error body; the id is valid as a search hit but stale by the time the item fetch runs.","commonSituations":"A thread flagged/deleted shortly after posting; Algolia eventual-consistency lag between the search index and the items index; rate limiting after many sequential scans; an intermediary returning an HTML error page that parsed to a non-object.","solutions":["Retry the scan — if the threadId was transiently bad, the next run resolves a fresh one.","Confirm the item endpoint manually: open https://hn.algolia.com/api/v1/items/{threadId} and check it returns an object with children[].","If the thread is genuinely dead/deleted, wait for the next monthly thread (this is a data condition, not a code bug).","Ensure ctx.fetchJson surfaces non-2xx as errors rather than returning parsed error bodies."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// After resolving threadId, sanity-check the item before relying on it.\nconst item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });\nif (!item || typeof item !== 'object') {\n  throw new Error(`hackernews: thread ${threadId} returned a non-object item — likely dead/deleted`);\n}","typeGuard":"/** A valid Algolia item response: an object (children[] checked separately). */\nfunction isHnItem(value) {\n  return !!value && typeof value === 'object' && !Array.isArray(value);\n}","tryCatchPattern":"try {\n  return await hackernewsProvider.fetch(entry, ctx);\n} catch (err) {\n  if (/unexpected item response for thread/.test(err.message)) {\n    // The threadId may point at a now-deleted story; a fresh run resolves a new id.\n    console.warn(`hackernews: transient item response — will retry next scan`);\n    return [];\n  }\n  throw err;\n}","preventionTips":["Treat a non-object item as transient (deleted/stale id) and let the next scan resolve a fresh thread.","Confirm the Algolia items endpoint directly when the error persists to rule out a rate limit returning a non-object body.","Keep ctx.fetchJson strict: surface non-2xx as rejections so error pages do not reach the shape check."],"tags":["network","external-api","hackernews","data-integrity"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}