jackwener/OpenCLI · error · CommandExecutionError

Flomo API returned malformed JSON: ${err instanceof Error ?

Error message

Flomo API returned malformed JSON: ${err instanceof Error ? err.message : String(err)}

What it means

fetchFlomoJson in clis/flomo/memos.js:167 wraps resp.json() in try/catch. When the Flomo API responds with HTTP 200 (or any ok status) but the body cannot be parsed as JSON, resp.json() throws and the library rethrows it as a CommandExecutionError with the underlying parse message. This guards against proxies, WAFs, captive portals, or Flomo serving HTML/empty bodies instead of the expected JSON envelope.

Source

Thrown at clis/flomo/memos.js:167

      headers: {
        Authorization: 'Bearer ' + token,
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        Accept: 'application/json',
      },
    });
  } catch (err) {
    throw new CommandExecutionError(`Failed to fetch Flomo memos: ${err instanceof Error ? err.message : String(err)}`);
  }
  if (resp.status === 401 || resp.status === 403) {
    throw new AuthRequiredError(FLOMO_API_DOMAIN, `Flomo API returned HTTP ${resp.status}; please refresh your Flomo login session`);
  }
  if (!resp.ok) {
    throw new CommandExecutionError(`Flomo API returned HTTP ${resp.status}`);
  }
  try {
    return await resp.json();
  } catch (err) {
    throw new CommandExecutionError(`Flomo API returned malformed JSON: ${err instanceof Error ? err.message : String(err)}`);
  }
}

async function readAccessToken(page) {
  const token = unwrapBrowserResult(await page.evaluate(buildGetTokenJs()));
  if (typeof token !== 'string' || !token.trim()) {
    throw new AuthRequiredError(FLOMO_API_DOMAIN, 'Flomo memos requires an active signed-in Flomo browser session');
  }
  return token.trim();
}

const command = cli({
  site: 'flomo',
  name: 'memos',
  access: 'read',
  description: 'List your Flomo memos',
  domain: FLOMO_API_DOMAIN,
  strategy: Strategy.COOKIE,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-run the command after checking network path (disable VPN/proxy or use a direct connection) so the real JSON response reaches fetch()
  2. Open https://v.flomoapp.com/ in the controlled browser and re-authenticate so Cloudflare challenges are satisfied, then retry
  3. Inspect the actual response body with curl using the same signed URL and Bearer token to see what non-JSON content is returned
  4. If it persists, capture the inner message (from err.message) and report it — it may indicate a Flomo API format change requiring a library update

Example fix

// before (debugging raw failure)
const body = await fetchFlomoJson(buildSignedUrl(limit, since, slug), token);

// after (capture body text for diagnosis before parsing)
const resp = await fetch(url, { headers: { Authorization: 'Bearer ' + token } });
const text = await resp.text();
let body;
try { body = JSON.parse(text); }
catch (err) {
  console.error('Non-JSON response:', text.slice(0, 500));
  throw new CommandExecutionError('Flomo API returned malformed JSON: ' + err.message);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check network reachability and content type before trusting the JSON
const head = await fetch('https://flomoapp.com/', { method: 'HEAD' }).catch(() => null);
if (!head || !head.ok) {
  throw new Error('flomoapp.com unreachable — check VPN/proxy/network before running the command');
}

Type guard

function isJsonObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try {
  await runFlomoMemos();
} catch (err) {
  if (err instanceof CommandExecutionError && /malformed JSON/i.test(err.message)) {
    // non-JSON 200 body: proxy/Cloudflare interference — retry once after checking network,
    // or re-authenticate via browser to clear bot challenges
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling the flomo memos command when the HTTPS response to https://flomoapp.com/api/v1/memo/updated/ has an ok status but a non-JSON body: an HTML login/Cloudflare challenge page, an empty body, a truncated response, or a proxy/error page returned with status 200.

Common situations: Corporate proxies or firewalls injecting HTML into 200 responses; VPN or captive portal intercepting the request; Cloudflare bot challenge served to the non-browser fetch(); network middleware stripping the body; Flomo API schema/CDN changes returning plain text or HTML.

Understand the failure class

Related errors


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