{"record":{"id":"653808ae73961e8b","repo":"jackwener/OpenCLI","slug":"sales-navigator-messaging-thread-api-returned-malf-653808","errorCode":null,"errorMessage":"Sales Navigator messaging thread API returned malformed messages","messagePattern":"Sales Navigator messaging thread API returned malformed messages","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/salesnav-thread.js","lineNumber":94,"sourceCode":"  const participants = Array.isArray(thread?.participants) ? thread.participants : Object.keys(resolution);\n  const byUrn = new Map();\n  for (const urn of participants) {\n    const profile = resolution[urn] || { entityUrn: urn };\n    byUrn.set(urn, profile);\n  }\n  return byUrn;\n}\n\nfunction parseSalesnavThreadMessages(thread) {\n  if (!thread || typeof thread !== 'object') {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed payload');\n  }\n  const threadId = normalizeWhitespace(thread?.id || '');\n  if (!threadId) {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned a thread without id');\n  }\n  if (!Array.isArray(thread?.messages)) {\n    throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed messages');\n  }\n  const byUrn = participantIndex(thread);\n  const messages = thread.messages;\n  const rows = messages.map((message) => {\n    if (!message || typeof message !== 'object') {\n      throw new CommandExecutionError('Sales Navigator messaging thread API returned malformed message row');\n    }\n    const deliveredAt = Number(message?.deliveredAt || 0);\n    const senderProfile = byUrn.get(message?.author);\n    return {\n      message_id: normalizeWhitespace(message?.id || ''),\n      thread_id: threadId,\n      sender: participantName(senderProfile) || normalizeWhitespace(message?.author || ''),\n      sender_urn: normalizeWhitespace(message?.author || ''),\n      text: normalizeWhitespace(message?.body || message?.systemMessageContent || ''),\n      subject: normalizeWhitespace(message?.subject || ''),\n      timestamp: deliveredAt ? new Date(deliveredAt).toISOString() : '',\n      delivered_at: deliveredAt || '',","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/salesnav-thread.js#L76-L112","documentation":"Thrown by parseSalesnavThreadMessages (clis/linkedin/salesnav-thread.js:94) when thread.messages is present but not an array. The parser requires an array to map each message into an output row. This indicates the API payload deviates from the expected decoration shape, typically because the response came from a different endpoint/version or was partially decoded.","triggerScenarios":"The decoration version mismatch relocates messages into a nested object or renames the field (e.g. messagesResolutionResults); fetchSalesnavJson returned an error/HTML-parsed object where messages is a string; caller passes a thread summary object that omits messages entirely (undefined fails the Array.isArray check).","commonSituations":"LinkedIn Sales Navigator redeploys changing the message container; calling the parser with a thread object built from the wrong endpoint; messageCount=0 edge responses where the field is omitted rather than returned as [].","solutions":["Re-run the command; if it recurs, the payload shape has changed rather than being transient.","Inspect the raw response (log Object.keys(thread) and typeof thread.messages) to find where messages now live under the current decoration.","Update THREAD_DECORATION from a live /sales-api/salesApiThreads network capture so the decoder restores the messages array.","Confirm you parsed the thread-detail endpoint response; inbox-list rows keep messages elsewhere and will fail this check."],"exampleFix":"// before\nconst rows = parseSalesnavThreadMessages(thread);\n// after (tolerate missing messages for empty threads)\nconst safeThread = { ...thread, messages: Array.isArray(thread.messages) ? thread.messages : [] };\nconst rows = parseSalesnavThreadMessages(safeThread);","handlingStrategy":"type-guard","validationCode":"// Coerce/guard messages before parsing:\nconst safeThread = { ...thread, messages: Array.isArray(thread.messages) ? thread.messages : [] };\n// note: an empty array still means 'no messages', which the caller should expect","typeGuard":"function hasMessagesArray(t) {\n  return t !== null && typeof t === 'object' && Array.isArray(t.messages);\n}","tryCatchPattern":"try {\n  const messages = parseSalesnavThreadMessages(thread);\n} catch (err) {\n  if (/malformed messages/.test(err.message)) {\n    console.error('messages is not an array — check decoration version and raw payload keys:', Object.keys(thread || {}));\n    throw err;\n  }\n  throw err;\n}","preventionTips":["Log Object.keys(thread) and typeof thread.messages whenever this error occurs to detect decoration drift early.","Confirm messageCount handling: zero-message threads may omit the field; normalize to [] upstream if that outcome is valid.","Refresh THREAD_DECORATION from a live capture after any Sales Navigator behavior change."],"tags":["linkedin","sales-navigator","api-response","schema-validation","malformed-payload"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}