{"record":{"id":"f9b70a2e304d5aa1","repo":"jackwener/OpenCLI","slug":"linkedin-messaging-api-returned-malformed-normaliz","errorCode":null,"errorMessage":"LinkedIn messaging API returned malformed normalized payload: missing included array","messagePattern":"LinkedIn messaging API returned malformed normalized payload: missing included array","errorType":"error_code","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/linkedin/inbox.js","lineNumber":65,"sourceCode":"        accept: 'application/vnd.linkedin.normalized+json+2.1',\n        'x-restli-protocol-version': '2.0.0',\n      },\n    });\n    if (res.status === 401 || res.status === 403) return { authRequired: true, error: 'HTTP ' + res.status };\n    if (!res.ok) return { error: 'HTTP ' + res.status };\n    return { json: await res.json() };\n  } catch (e) {\n    return { error: 'fetch failed: ' + ((e && e.message) || String(e)) };\n  }\n}\n\n// Parse LinkedIn's normalized messaging JSON into plain conversation rows.\n// `included` is a flat entity array; conversations reference participants and\n// messages by URN, which we resolve through a urn->entity index. Exported for\n// unit testing against a captured fixture.\nfunction parseConversations(normalized, mailboxUrn) {\n  if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized) || !Array.isArray(normalized.included)) {\n    throw new CommandExecutionError('LinkedIn messaging API returned malformed normalized payload: missing included array');\n  }\n  const included = normalized.included;\n  const byUrn = new Map();\n  for (const o of included) {\n    if (o && o.entityUrn) byUrn.set(o.entityUrn, o);\n  }\n  const norm = (s) => String(s == null ? '' : s).replace(/\\s+/g, ' ').trim();\n\n  const participantInfo = (p) => {\n    if (!p) return { name: '', kind: '' };\n    const pt = p.participantType || {};\n    if (pt.organization && pt.organization.name) return { name: norm(pt.organization.name.text), kind: 'organization' };\n    if (pt.member) {\n      const fn = pt.member.firstName && pt.member.firstName.text;\n      const ln = pt.member.lastName && pt.member.lastName.text;\n      return { name: norm([fn, ln].filter(Boolean).join(' ')), kind: 'member' };\n    }\n    if (pt.agent && pt.agent.name) return { name: norm(pt.agent.name.text), kind: 'agent' };","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/linkedin/inbox.js#L47-L83","documentation":"parseConversations() in inbox.js parses LinkedIn's normalized messenger JSON (accept: application/vnd.linkedin.normalized+json+2.1) and requires normalized.included to be an array — the flat entity list from which conversations, participants and messages are resolved by URN. Any non-object payload or missing/included non-array triggers this CommandExecutionError.","triggerScenarios":"Re-issuing the messengerConversations GraphQL request when the response is an error JSON object, an empty body parsed as something unexpected, or LinkedIn changes the normalized envelope (e.g. renamed included), including when a captured fixture/unit test passes a malformed payload.","commonSituations":"LinkedIn schema change altering the top-level shape; the request URL lifted from the Performance API returning an error envelope with HTTP 200; acceptance header mismatch causing plain JSON instead of normalized JSON; stale fixtures in tests.","solutions":["Retry the command — reload /messaging/ so a fresh messengerConversations URL and token are captured.","Check whether the response was an error envelope (JSON with message/status) rather than normalized data.","Update the CLI; if LinkedIn renamed the envelope, a newer version may parse the new shape.","In tests, regenerate the captured fixture from a live session instead of using an outdated one."],"exampleFix":"// before (fixture outdated)\nparseConversations({}, 'urn:li:fsd_mailbox:me');\n// Error: ...missing included array\n// after\nparseConversations({ included: [/* entities from a fresh capture */] }, 'urn:li:fsd_mailbox:me');","handlingStrategy":"type-guard","validationCode":"function isNormalizedPayload(p) {\n  return p && typeof p === 'object' && !Array.isArray(p) && Array.isArray(p.included);\n}\n// before calling parseConversations on a captured/raw payload:\nif (!isNormalizedPayload(raw)) throw new Error('Payload is not normalized messaging JSON.');","typeGuard":"function isNormalizedMessagingJson(value) {\n  return typeof value === 'object' && value !== null && !Array.isArray(value) && Array.isArray(value.included);\n}","tryCatchPattern":"try {\n  const convs = await opencli.linkedin.inbox({ limit: 20 });\n} catch (e) {\n  if (/missing included array/.test(e.message || '')) {\n    console.warn('Messaging payload was not normalized JSON — reload /messaging/ and retry.');\n    return [];\n  }\n  throw e;\n}","preventionTips":["Always reload /messaging/ so the captured request URL and token are current.","Regenerate test fixtures from live sessions when LinkedIn ships changes.","Check for JSON error envelopes (message/status keys) when this fires with HTTP 200.","Keep the Accept header as application/vnd.linkedin.normalized+json+2.1."],"tags":["linkedin","schema-validation","messaging"],"backgroundTag":"missing-response-field","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}