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
- Re-run the command after checking network path (disable VPN/proxy or use a direct connection) so the real JSON response reaches fetch()
- Open https://v.flomoapp.com/ in the controlled browser and re-authenticate so Cloudflare challenges are satisfied, then retry
- Inspect the actual response body with curl using the same signed URL and Bearer token to see what non-JSON content is returned
- 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
- Run the command on a clean network path; disable intercepting VPNs, proxies, and captive portals
- Authenticate via the managed browser first so Cloudflare/bot challenges are cleared before fetch() calls
- Log the raw response body when debugging instead of only the parse error
- Keep the CLI updated in case Flomo changes response formats
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Instagram private publish ${stage} returned invalid JSON
- 12306 queryByTrainNo returned non-JSON body
- 12306 ${endpoint} returned non-JSON body
- ${label} returned HTTP ${resp.status}: ${summarizeApiError(p
- arXiv API HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/67c4182a59a43799.
Report an issue: GitHub.