{"record":{"id":"a2dc478b3e7de544","repo":"BloopAI/vibe-kanban","slug":"fallback-response-for-table-is-not-an-object","errorCode":null,"errorMessage":"Fallback response for \"${table}\" is not an object","messagePattern":"Fallback response for \"(.+?)\" is not an object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/web-core/src/shared/lib/electric/collections.ts","lineNumber":412,"sourceCode":"\n  for (const row of rows) {\n    syncParams.write({\n      type: 'insert',\n      value: row,\n      metadata: {},\n    });\n  }\n\n  syncParams.commit();\n  syncParams.markReady();\n}\n\nfunction extractFallbackRows(\n  payload: unknown,\n  table: string\n): Array<ElectricRow> {\n  if (!payload || typeof payload !== 'object') {\n    throw new Error(`Fallback response for \"${table}\" is not an object`);\n  }\n\n  const rows = (payload as Record<string, unknown>)[table];\n  if (!Array.isArray(rows)) {\n    throw new Error(`Fallback response missing \"${table}\" array`);\n  }\n\n  return rows as Array<ElectricRow>;\n}\n\nasync function parseResponseError(\n  response: Response,\n  fallbackMessage: string\n): Promise<string> {\n  try {\n    const body = (await response.json()) as {\n      message?: string;\n      error?: string;","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/web-core/src/shared/lib/electric/collections.ts#L394-L430","documentation":"extractFallbackRows validates the JSON payload returned by a table's fallback endpoint: it must be an object, because the row array is looked up under the payload[table] key. This error is thrown when the fallback HTTP endpoint succeeded (2xx) but its body is not a JSON object (e.g. null, an array, a string, or invalid shape parsed as a primitive).","triggerScenarios":"Fallback endpoint returns a bare JSON array instead of { [table]: [...] }; endpoint returns null; endpoint returns plain text/HTML with 200; response.json() parses to a primitive; a proxy intercepts and returns a non-standard body.","commonSituations":"Backend route returning `res.json(rows)` instead of keyed object; API gateway/error page served with 200; mismatch between the shape's `table` name and the key the server actually uses (e.g. table 'tasks' but server returns { items: [...] }); server upgrade changed response envelope.","solutions":["Make the fallback endpoint return { \"<table>\": [ ...rows ] } keyed by the shape's table name.","Verify the `table` value in the ShapeDefinition matches the key the server uses in the response.","Log the actual payload on failure to see what the endpoint returned.","Check for proxies/middleware rewriting successful responses (auth walls returning HTML with 200)."],"exampleFix":"// before (server)\nres.json(rows);\n// after (server)\nres.json({ tasks: rows }); // key must equal the shape's table name","handlingStrategy":"type-guard","validationCode":"const res = await fetch(fallbackUrl);\nconst payload = await res.json();\nif (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n  throw new Error('Fallback endpoint must return an object keyed by table name');\n}","typeGuard":"function isFallbackPayload(payload: unknown, table: string): payload is Record<string, ElectricRow[]> {\n  return (\n    typeof payload === 'object' && payload !== null && !Array.isArray(payload) &&\n    Array.isArray((payload as Record<string, unknown>)[table])\n  );\n}","tryCatchPattern":"try {\n  const payload = await response.json();\n  if (!isFallbackPayload(payload, table)) {\n    console.error('Unexpected fallback payload', payload);\n    return;\n  }\n} catch (e) {\n  reportSyncError(e);\n}","preventionTips":["Contract-test every fallback endpoint's response shape","Keep the response key identical to the shape's table name","Never return bare arrays, null, or HTML with 200 from fallback routes","Log raw payloads when adding new fallback sources"],"tags":["validation","json","electric-sql","fallback-sync"],"backgroundTag":"unexpected-response-shape","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}