{"record":{"id":"e08a5ec7288b872b","repo":"jackwener/OpenCLI","slug":"expected-array-of-rows-from-server-got-typeof-r","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/bookmark-list.js","lineNumber":41,"sourceCode":"  columns: ['id', 'messageId', 'content', 'savedAt'],\n  func: async (page, kwargs) => {\n    const limit = parsePositiveInteger(kwargs.limit, '--limit', { defaultValue: 50 });\n    const offset = parseNonNegativeInteger(kwargs.offset, '--offset', { defaultValue: 0 });\n    await page.goto(SLOCK_HOME_URL);\n    const snippet = `\n      ${authHeadersFragment({ serverScoped: true, serverIdOverride: kwargs.server })}\n      const res = await fetch('${SLOCK_API_BASE}/channels/saved?limit=' + encodeURIComponent(${JSON.stringify(limit)}) + '&offset=' + encodeURIComponent(${JSON.stringify(offset)}), { credentials:'include', headers });\n      if (!res.ok) return { kind: res.status===401?'auth':'http', status: res.status, where:'/channels/saved' };\n      const data = await res.json();\n      // F3-b — qatester live dump: shape is { saved: [...], hasMore }.\n      // Unwrap .saved first; fall back to legacy .bookmarks / .data /\n      // bare array for forward-compat.\n      return { kind: 'ok', rows: Array.isArray(data) ? data : (data.saved || data.bookmarks || data.data || []) };\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((b) => ({\n      id: b.id ?? '',\n      messageId: b.messageId ?? '',\n      content: b.content ?? b.message?.content ?? '',\n      savedAt: b.savedAt ?? b.createdAt ?? '',\n    }));\n  },\n});\n","sourceCodeStart":23,"sourceCodeEnd":51,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/slock/bookmark-list.js#L23-L51","documentation":"This CommandExecutionError is thrown when dispatchEvaluateResult returns something that is not an array of bookmark rows. The bookmark-list command's contract says the /channels/saved response must dispatch to an array; anything else means the server response shape changed (contract drift) or the envelope was misparsed.","triggerScenarios":"The server returns an object for the saved-list endpoint that dispatchEvaluateResult maps to a non-array (e.g. a pagination envelope {items, total}), or the snippet's fallback extraction (data.saved || data.bookmarks || data.data || []) misses the new key.","commonSituations":"A Slock server upgrade introducing a paginated/renamed response, a proxy injecting an HTML error page that parses oddly, or an older server whose field names differ from all the fallbacks.","solutions":["Log the raw result before dispatch to see the actual response shape from /channels/saved.","Upgrade or downgrade the CLI so its parsing matches your server's API version.","If the server renamed the collection field, patch the snippet's fallback chain (add the new key).","Retry after confirming you're hitting the right server (serverScoped override) — some deployments have divergent shapes."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"const isRowArray = (v) => Array.isArray(v) && v.every((x) => x != null && typeof x === 'object');","tryCatchPattern":"try {\n  const bookmarks = await bookmarkList(page);\n} catch (e) {\n  if (e instanceof CommandExecutionError && e.message.includes('contract drift')) {\n    console.error('Response shape changed — check server/CLI version match:', e.message);\n  } else throw e;\n}","preventionTips":["Keep CLI and server versions aligned.","Inspect the raw /channels/saved response after any server upgrade.","Guard downstream .map() calls with Array.isArray checks of your own."],"tags":["api-contract","response-shape","version-mismatch","pagination"],"backgroundTag":"api-contract-drift","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}