{"record":{"id":"12f4b543125dc643","repo":"jackwener/OpenCLI","slug":"expected-array-of-rows-from-server-got-typeof-r-12f4b5","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-server.js","lineNumber":52,"sourceCode":"      throw new ArgumentError(`status \"${status}\" not in {${TASK_STATUSES.join('|')}}`);\n    }\n    await page.goto(SLOCK_HOME_URL);\n    const snippet = `\n      ${authHeadersFragment({ serverScoped: true, serverIdOverride: kwargs.server })}\n      const status = ${JSON.stringify(status)};\n      const qs = status ? ('?status=' + encodeURIComponent(status)) : '';\n      const res = await fetch('${SLOCK_API_BASE}/tasks/server' + qs, { credentials:'include', headers });\n      if (!res.ok) return { kind: res.status===401?'auth':'http', status: res.status, where: '/tasks/server' };\n      const data = await res.json();\n      if (!data || !Array.isArray(data.tasks)) {\n        return { kind: 'http', status: 200, where: '/tasks/server (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      channelId: t.channelId ?? null,\n      assigneeId: t.claimedById ?? t.assigneeId ?? null,\n    }));\n  },\n});\n","sourceCodeStart":34,"sourceCodeEnd":64,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/slock/task-list-server.js#L34-L64","documentation":"After evaluating the in-page fetch snippet, task-list-server unwraps the result with dispatchEvaluateResult and asserts it is an array of task rows. If the browser-side script resolved to anything else (object, string, undefined), the command throws a CommandExecutionError flagging likely contract drift between the CLI's expected response shape and what the server returned.","triggerScenarios":"The slock API returns a payload where `data.tasks` is missing/not an array (error envelope, HTML error page, paginated object, renamed field), or the snippet's `{ kind: 'ok', rows }` path changed so dispatchEvaluateResult yields a non-array.","commonSituations":"Slock server upgraded with a new response schema (e.g. tasks moved under data.items or wrapped in pagination); auth failure returning an error object with HTTP 200; a proxy/WAF intercepting the request; local snippet edited out of sync with dispatchEvaluateResult.","solutions":["Log the raw `result` from page.evaluate to inspect the actual server payload","Check the slock API version for schema changes and update the snippet's row extraction (data.tasks → new path)","Confirm auth headers/session are valid — a 200 with an error body is the usual culprit","Pin the working slock server version or add explicit shape normalization before the Array.isArray check"],"exampleFix":"// before\nconst rows = dispatchEvaluateResult(result);\nif (!Array.isArray(rows)) throw new CommandExecutionError(...);\n// after\nconst payload = dispatchEvaluateResult(result);\nconst rows = Array.isArray(payload) ? payload : (Array.isArray(payload?.tasks) ? payload.tasks : null);\nif (!rows) throw new CommandExecutionError(`unexpected payload: ${JSON.stringify(payload).slice(0, 200)}`);","handlingStrategy":"type-guard","validationCode":"const payload = dispatchEvaluateResult(result);\nif (payload != null && !Array.isArray(payload) && Array.isArray(payload.tasks)) {\n  // tolerate wrapped responses before the CLI's strict check\n}","typeGuard":"const isTaskRowArray = (v) => Array.isArray(v) && v.every((r) => r != null && typeof r === 'object');","tryCatchPattern":"try {\n  const rows = await listServerTasks(opts);\n} catch (e) {\n  if (/expected array of rows/.test(e.message)) {\n    // log raw server payload and check API version/schema\n  }\n  throw e;\n}","preventionTips":["Log raw API responses during slock server upgrades","Pin compatible CLI/server versions","Add a contract test asserting the tasks endpoint returns an array"],"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"}