{"record":{"id":"4c12d9d8fb26cef0","repo":"jackwener/OpenCLI","slug":"sales-navigator-messaging-thread-api-returned-malf-4c12d9","errorCode":null,"errorMessage":"Sales Navigator messaging thread API returned malformed message row","messagePattern":"Sales Navigator messaging thread API returned malformed message row","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/salesnav-thread.js","lineNumber":100,"sourceCode":"  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 || '',\n      type: normalizeWhitespace(message?.type || ''),\n    };\n  }).filter((row) => row.text || row.subject || row.message_id);\n  rows.sort((a, b) => Number(a.delivered_at || 0) - Number(b.delivered_at || 0));\n  return rows.map((row, index) => ({ index, ...row }));\n}","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/salesnav-thread.js#L82-L118","documentation":"Thrown inside the messages.map callback of parseSalesnavThreadMessages (clis/linkedin/salesnav-thread.js:100) when any element of thread.messages is null or not an object. Each message must be an object carrying id, author, body/deliveredAt fields to build an output row. The library fails loudly on the first bad row instead of emitting rows with undefined fields, so one corrupt element aborts the whole parse.","triggerScenarios":"The API returned an array containing null placeholders (e.g. deleted/redacted messages serialized as null); a decoration mismatch causing message entries to be decoded as strings or arrays; pagination stitching in fetchThreadWithPagination concatenating heterogeneous payloads where one page's element is not an object.","commonSituations":"Threads containing deleted or system-redacted messages that LinkedIn serializes as null entries; mixed-version decorations after a Sales Navigator redeploy; custom code merging thread pages before parsing and injecting undefined entries.","solutions":["Re-run the command to see if the null row is transient; then compare which thread/message triggers it (the index before failure localizes the corrupt message).","Filter non-object message entries before parsing: thread.messages.filter((m) => m && typeof m === 'object').","Refresh THREAD_DECORATION from a live network capture if message fields are being decoded into wrong types.","If a specific thread always fails (e.g. contains deleted messages), skip that thread or report it upstream so the parser can tolerate null rows."],"exampleFix":"// before (one null row aborts everything)\nconst messages = parseSalesnavThreadMessages(thread);\n// after (drop non-object rows first)\nconst cleaned = { ...thread, messages: (thread.messages || []).filter((m) => m && typeof m === 'object') };\nconst messages = parseSalesnavThreadMessages(cleaned);","handlingStrategy":"type-guard","validationCode":"// Sanitize message rows before parsing:\nconst safeThread = {\n  ...thread,\n  messages: (thread.messages || []).filter((m) => m && typeof m === 'object'),\n};","typeGuard":"function isMessageObject(m) {\n  return m !== null && typeof m === 'object' && !Array.isArray(m);\n}","tryCatchPattern":"try {\n  const messages = parseSalesnavThreadMessages(thread);\n} catch (err) {\n  if (/malformed message row/.test(err.message)) {\n    // drop non-object rows (deleted/redacted messages serialize as null) and retry\n    const cleaned = { ...thread, messages: thread.messages.filter(isMessageObject) };\n    return parseSalesnavThreadMessages(cleaned);\n  }\n  throw err;\n}","preventionTips":["Filter out null/non-object message entries before parsing — threads with deleted messages can contain them.","If one specific thread always fails, isolate its raw payload; it may need special handling upstream.","Avoid merging thread pages with spread/concat bugs that can inject undefined entries into messages."],"tags":["linkedin","sales-navigator","api-response","schema-validation","malformed-row"],"backgroundTag":"schema-validation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}