jackwener/OpenCLI · error · CommandExecutionError

LinkedIn thread-snapshot returned a malformed API discovery

Error message

LinkedIn thread-snapshot returned a malformed API discovery payload.

What it means

The discovery step returns the intercepted thread info including the captured apiUrls. The command throws this CommandExecutionError when discovery is null/not an object/is an array, or lacks a valid apiUrls array — i.e. the scraper could not identify any messengerMessages network requests for the open thread.

Source

Thrown at clis/linkedin/thread-snapshot.js:339

    if (discovery && Array.isArray(discovery.apiUrls) && discovery.apiUrls.length === 0) {
      const firstDiscovery = discovery;
      await page.wait(4);
      const retried = unwrapEvaluateResult(await page.evaluate(buildThreadApiDiscoveryScript(0)));
      if (retried && typeof retried === 'object' && !Array.isArray(retried)) {
        discovery = {
          ...retried,
          scrollAttempts: firstDiscovery.scrollAttempts,
          scrollStable: firstDiscovery.scrollStable,
        };
      } else {
        discovery = retried;
      }
    }
    if (discovery?.authRequired) {
      throw new AuthRequiredError(LINKEDIN_DOMAIN, 'LinkedIn thread-snapshot requires an active signed-in LinkedIn browser session.');
    }
    if (!discovery || typeof discovery !== 'object' || Array.isArray(discovery) || !Array.isArray(discovery.apiUrls)) {
      throw new CommandExecutionError('LinkedIn thread-snapshot returned a malformed API discovery payload.');
    }

    const actualUrl = canonicalizeLinkedInThreadUrl(discovery.url || '');
    if (threadUrl && actualUrl && threadUrl !== actualUrl) {
      throw new CommandExecutionError('LinkedIn thread-snapshot blocked: thread_url_mismatch', `Expected ${threadUrl}; actual ${actualUrl}`);
    }
    if (maxScrolls >= 4 && discovery.scrollAttempts >= maxScrolls && discovery.scrollStable === false) {
      throw new CommandExecutionError('LinkedIn thread history did not stabilize before --max-scrolls; refusing to return a partial snapshot.');
    }

    const apiUrls = validateThreadApiUrls(discovery.apiUrls, threadUrl);
    const csrf = await requireLinkedInCookie(page, 'LinkedIn thread-snapshot');
    const fetched = unwrapEvaluateResult(
      await page.evaluate(buildFetchThreadPagesScript(apiUrls, csrf)),
    );
    if (fetched?.authRequired) {
      throw new AuthRequiredError(LINKEDIN_DOMAIN, `LinkedIn messengerMessages API authentication failed: ${fetched.error}`);
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify --thread-url is a canonical LinkedIn messaging thread URL (linkedin.com/messaging/thread/...)
  2. Increase wait time / max-scrolls so the API requests actually fire and are captured before parsing
  3. Disable proxies/extensions that might block or rewrite voyager API requests
  4. Update the library if LinkedIn renamed the GraphQL endpoint or queryId pattern

Example fix

// before: thread URL that never triggers messengerMessages
await run('linkedin thread-snapshot', { 'thread-url': 'https://www.linkedin.com/messaging/' });
// after: exact thread URL
await run('linkedin thread-snapshot', { 'thread-url': 'https://www.linkedin.com/messaging/thread/2-Yzg1Zj...' });
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the URL is a messaging thread URL that triggers the messengerMessages API
const u = new URL(threadUrl);
if (u.hostname !== 'www.linkedin.com' || !u.pathname.startsWith('/messaging/thread/')) {
  throw new Error('Provide a canonical LinkedIn messaging thread URL');
}

Type guard

const isMessagingThreadUrl = (v) => {
  try { const u = new URL(v); return u.protocol === 'https:' && u.hostname === 'www.linkedin.com' && u.pathname.startsWith('/messaging/thread/'); }
  catch { return false; }
};

Try / catch

try {
  const snap = await run('linkedin thread-snapshot', { 'thread-url': url });
} catch (err) {
  if (String(err.message).includes('malformed API discovery payload')) {
    // no messengerMessages requests captured: increase waits/scrolls and retry
  } else throw err;
}

Prevention

When it happens

Trigger: The page loaded but no matching GraphQL request was captured (network interception failed, wrong URL passed, messaging page failed to render); a retry still returned a malformed shape; discovery returned an unexpected object after LinkedIn's frontend changed its request pattern.

Common situations: Slow page load — scrolls finished before the API fired; browser extension or proxy blocking voyager API calls; LinkedIn UI changes so no request matches the messengerMessages pattern; passing a non-messaging URL that never triggers the API.

Understand the failure class

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/21c71984ebce0de5. Report an issue: GitHub.