santifer/career-ops · error · Error
hackernews: unexpected item response for thread
Error message
hackernews: unexpected item response for thread ${threadId} What it means
After resolving the thread id, the provider fetches the thread item from the HN (Algolia items) API and requires a JSON object back. If the response is missing or not an object (null, array, string, error payload), fetch() throws this error naming the thread id. Non-object items mean the thread could not be retrieved and children cannot be parsed.
Solutions
- Open https://news.ycombinator.com/item?id={threadId} to confirm the thread still exists.
- Call the item endpoint directly (hn.algolia.com/api/v1/items/{id}) and inspect the raw response.
- Retry later if Algolia is rate-limiting or degraded; add a small backoff between requests.
- Update the item parsing path in providers/hackernews.mjs if the API shape changed.
Example fix
// before
const item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
// after (retry once on bad shape)
let item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
if (!item || typeof item !== 'object') item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' }); Defensive patterns
Strategy: type-guard
Validate before calling
function isItemObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
const item = await (await fetch(itemUrl(id))).json();
if (!isItemObject(item)) throw new Error(`bad item payload for ${id}`); Type guard
const isHnItem = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && ('id' in v || 'children' in v); Try / catch
try {
await provider.fetch(entry, ctx);
} catch (err) {
if (/hackernews: unexpected item response/.test(err.message)) {
console.error(`Thread item ${err.message.split('thread ')[1]} unusable — verify it still exists on news.ycombinator.com`);
return;
}
throw err;
} Prevention
- Re-resolve the thread id via search on every run instead of caching a stale id.
- Back off and retry on rate-limited or degraded Algolia responses.
- Check that fetchJson rejects HTML/error bodies before they reach the object check.
- Keep a link to the thread id in logs so it can be checked manually.
When it happens
Trigger: ctx.fetchJson(itemUrl(threadId)) returns null or a non-object: deleted/dead thread id, Algolia item API returning an error JSON or empty body, or a proxy response swallowed as a string.
Common situations: Stale cached thread id used after a thread was removed, Algolia item endpoint outage or rate limit, and redirects/blocked requests whose body parses to something other than the expected item object.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- glints: unexpected API response
- hackernews: could not find "Ask HN: Who is hiring?" thread…
- Apify did not return a run id
- getonbrd: unexpected API response for category
- solidjobs: unexpected API response — expected
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/6210d5140243ca7a.
Report an issue: GitHub.
Appendix: source
Thrown at providers/hackernews.mjs:154
/** @type {Provider} */
export default {
id: 'hackernews',
async fetch(entry, ctx) {
// Step 1: Find the latest "Who is hiring?" story id.
const searchData = await ctx.fetchJson(SEARCH_URL, { redirect: 'error' });
const threadId = resolveLatestThreadId(searchData);
if (!threadId) {
throw new Error('hackernews: could not find "Ask HN: Who is hiring?" thread in search results');
}
const threadHnUrl = `https://news.ycombinator.com/item?id=${threadId}`;
// Step 2: Fetch the thread item (children = top-level job comments).
const item = await ctx.fetchJson(itemUrl(threadId), { redirect: 'error' });
if (!item || typeof item !== 'object') {
throw new Error(`hackernews: unexpected item response for thread ${threadId}`);
}
const children = /** @type {any} */ (item).children;
if (!Array.isArray(children)) return [];
// Step 3: Parse each comment.
const jobs = [];
for (const child of children) {
// Skip deleted / dead / empty comments.
if (!child || child.deleted || child.dead) continue;
const text = typeof child.text === 'string' ? child.text : '';
if (!text.trim()) continue;
const parsed = parseHnComment(text, threadHnUrl);
if (!parsed) continue;
jobs.push({
title: parsed.title,View on GitHub (pinned to aac998c7ed)