{"record":{"id":"495d25ae5dddbeb9","repo":"jackwener/OpenCLI","slug":"expected-array-of-rows-from-server-got-typeof-r-495d25","errorCode":null,"errorMessage":"expected array of rows from server, got ${typeof rows} (contract drift?)","messagePattern":"expected array of rows from server, got (.+?) \\(contract drift\\?\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/slock/task-list.js","lineNumber":60,"sourceCode":"    await page.goto(SLOCK_HOME_URL);\n    const snippet = `\n      ${authHeadersFragment({ serverScoped: true, serverIdOverride: kwargs.server })}\n      ${channelResolveFragment(channel)}\n      const status = ${JSON.stringify(status)};\n      const qs = status ? ('?status=' + encodeURIComponent(status)) : '';\n      const tres = await fetch('${SLOCK_API_BASE}/tasks/channel/' + encodeURIComponent(channelId) + qs, { credentials:'include', headers });\n      if (!tres.ok) return { kind: tres.status===401?'auth':'http', status: tres.status, where: '/tasks/channel/:id' };\n      const data = await tres.json();\n      // Server contract: { tasks: [...] }. Reject anything else as drift.\n      if (!data || !Array.isArray(data.tasks)) {\n        return { kind: 'http', status: 200, where: '/tasks/channel/:id (expected {tasks:[]}, got drift)' };\n      }\n      return { kind: 'ok', rows: data.tasks };\n    `;\n    const result = await page.evaluate(`(async () => { ${snippet} })()`);\n    const rows = dispatchEvaluateResult(result);\n    if (!Array.isArray(rows)) {\n      throw new CommandExecutionError(`expected array of rows from server, got ${typeof rows} (contract drift?)`);\n    }\n    return rows.map((t) => ({\n      id: t.id ?? '',\n      taskNumber: t.taskNumber ?? null,\n      title: t.content ?? t.title ?? '',\n      taskStatus: t.taskStatus ?? t.status ?? '',\n      assigneeId: t.claimedById ?? t.assigneeId ?? null,\n    }));\n  },\n});\n","sourceCodeStart":42,"sourceCodeEnd":71,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/slock/task-list.js#L42-L71","documentation":"Same guard as task-list-server: after the in-page fetch resolves, task-list asserts the unwrapped result is an array of task rows. A non-array indicates the server response no longer matches the shape the snippet expects (`data.tasks` array), so the command throws a CommandExecutionError instead of mapping garbage rows.","triggerScenarios":"The channel-tasks endpoint returns an error envelope, object, or paginated wrapper instead of an array under data.tasks; snippet and dispatchEvaluateResult expectations diverge; auth rejected with a 200 error body.","commonSituations":"Slock API/schema upgrade renaming or wrapping `tasks`; proxy returning HTML; channel name resolving but response contract changed; stale cached CLI version against a newer server.","solutions":["Dump the raw page.evaluate result to see the real payload","Update the snippet's row extraction path to match the current API (e.g. data.tasks → data.items)","Verify auth/session validity; an error body with HTTP 200 commonly triggers this","Align CLI and server versions, or add normalization before the Array.isArray assertion"],"exampleFix":"// before\nreturn { kind: 'ok', rows: data.tasks };\n// after\nconst list = Array.isArray(data?.tasks) ? data.tasks : (Array.isArray(data?.items) ? data.items : []);\nreturn { kind: 'ok', rows: list };","handlingStrategy":"type-guard","validationCode":"const payload = dispatchEvaluateResult(result);\nif (!Array.isArray(payload) && payload?.tasks == null) {\n  console.error('Unexpected channel-tasks payload:', payload);\n}","typeGuard":"const isTaskRowArray = (v) => Array.isArray(v) && v.every((r) => r != null && typeof r === 'object');","tryCatchPattern":"try {\n  const rows = await listChannelTasks(channel, opts);\n} catch (e) {\n  if (/expected array of rows/.test(e.message)) {\n    // inspect raw payload; verify auth and API schema\n  }\n  throw e;\n}","preventionTips":["Keep the in-page snippet in sync with the server's response schema","Verify auth/session when payloads suddenly change shape","Add a contract test for the channel tasks endpoint"],"tags":["contract-drift","api-response","shape-mismatch"],"backgroundTag":"api-response-shape-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}