{"record":{"id":"8397a2c261c276e3","repo":"bytedance/deer-flow","slug":"thread-history-returned-an-invalid-response","errorCode":null,"errorMessage":"Thread history returned an invalid response.","messagePattern":"Thread history returned an invalid response\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/core/threads/hooks.ts","lineNumber":338,"sourceCode":"  data: RunMessage[];\n  has_more: boolean;\n  next_before_seq: number | null;\n};\n\nfunction isValidThreadMessageSeq(value: unknown): value is number {\n  return typeof value === \"number\" && Number.isSafeInteger(value) && value >= 1;\n}\n\n/**\n * Validate the sequence fields that history reconciliation and pagination use\n * as runtime identities. The static RunMessage type cannot protect this JSON\n * boundary from version skew or malformed responses.\n */\nexport function parseThreadMessagesPageResponse(\n  value: unknown,\n): ThreadMessagesPageResponse {\n  if (typeof value !== \"object\" || value === null) {\n    throw new Error(\"Thread history returned an invalid response.\");\n  }\n\n  const data = Reflect.get(value, \"data\");\n  const hasMore = Reflect.get(value, \"has_more\");\n  const nextBeforeSeq = Reflect.get(value, \"next_before_seq\");\n  if (!Array.isArray(data) || typeof hasMore !== \"boolean\") {\n    throw new Error(\"Thread history returned an invalid response.\");\n  }\n\n  const seenSeqs = new Set<number>();\n  for (const row of data) {\n    const seq =\n      typeof row === \"object\" && row !== null\n        ? Reflect.get(row, \"seq\")\n        : undefined;\n    if (!isValidThreadMessageSeq(seq)) {\n      throw new Error(\"Thread history returned a row with an invalid seq.\");\n    }","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/frontend/src/core/threads/hooks.ts#L320-L356","documentation":"Thrown by parseThreadMessagesPageResponse when the JSON from the thread-history endpoint does not match the ThreadMessagesPageResponse contract (object with data: unknown[], has_more: boolean, next_before_seq). The guard exists because the static RunMessage type cannot protect this boundary from version skew or malformed responses; per-row seq validation continues after these shape checks.","triggerScenarios":"Backend older/newer than the frontend expecting different history envelope fields; a proxy returning an HTML error page parsed as JSON object; has_more missing or data null; per-row seq missing, non-safe-integer, duplicated or < 1 in later checks.","commonSituations":"Frontend deployed ahead of backend after an upgrade; API gateway injecting wrapper objects; backend returning {error:...} with 200 during partial failures.","solutions":["Log the raw value at the boundary to see which field failed (value, data, has_more, or row seq)","Align frontend and backend versions (this envelope is version-coupled by design)","If a proxy wraps responses, configure it to pass /api/threads JSON through unmodified","On parse failure, retry the page request once and then surface a 'history unavailable' state rather than a crash"],"exampleFix":"// before\nconst page = parseThreadMessagesPageResponse(await res.json());\n\n// after\nlet page: ThreadMessagesPageResponse;\ntry {\n  page = parseThreadMessagesPageResponse(await res.json());\n} catch {\n  reportSchemaMismatch(threadId);\n  return emptyPage(); // graceful degradation for the history panel\n}","handlingStrategy":"type-guard","validationCode":"function looksLikeMessagesPage(v: unknown): boolean {\n  if (typeof v !== 'object' || v === null) return false;\n  return Array.isArray(Reflect.get(v, 'data')) && typeof Reflect.get(v, 'has_more') === 'boolean';\n}","typeGuard":"export function isThreadMessagesPage(v: unknown): v is ThreadMessagesPageResponse {\n  if (typeof v !== 'object' || v === null) return false;\n  const data = Reflect.get(v, 'data');\n  const hasMore = Reflect.get(v, 'has_more');\n  const nextBeforeSeq = Reflect.get(v, 'next_before_seq');\n  return (\n    Array.isArray(data) &&\n    typeof hasMore === 'boolean' &&\n    (nextBeforeSeq === null || nextBeforeSeq === undefined || (typeof nextBeforeSeq === 'number' && Number.isSafeInteger(nextBeforeSeq)))\n  );\n}","tryCatchPattern":"let raw: unknown = await res.json();\nif (!isThreadMessagesPage(raw)) {\n  reportHistorySchemaMismatch(threadId, raw);\n  return emptyHistoryPage(); // degrade instead of crash\n}\nreturn parseThreadMessagesPageResponse(raw);","preventionTips":["Pre-check the envelope with a type guard before the throwing parser","Pin frontend and backend versions together on deploy","Log the raw payload on mismatch to catch proxy wrappers early"],"tags":["threads","validation","schema","type-guard","frontend"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}