jackwener/OpenCLI · error · CommandExecutionError

Mercury returned malformed ${label}

Error message

Mercury returned malformed ${label}

What it means

assertObject validates that Mercury (the browser automation backend) returned a non-null, non-array object under the given label. If the value is null/undefined, a primitive, or an array, it throws CommandExecutionError with 'Mercury returned malformed <label>'. This guards the boundary between the CLI and Mercury's JSON responses, which can degrade if the page/extension state is unexpected.

Source

Thrown at clis/mercury/utils.js:109

        receipt: parseReceiptPath(kwargs),
        amount: parseAmount(kwargs),
        currency: parseCurrency(kwargs),
        date: parseDate(kwargs),
        merchant: requireString(kwargs, 'merchant'),
        category: optionalString(kwargs, 'category', 'Marketing & Advertising'),
        notes: requireString(kwargs, 'notes'),
        ocrWaitSeconds: parseOcrWaitSeconds(kwargs),
        closeAfterReview: optionalBoolean(kwargs, 'close-after-review', false),
    };
}

export function receiptBasename(receipt) {
    return path.basename(receipt);
}

export function assertObject(value, label) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        throw new CommandExecutionError(`Mercury returned malformed ${label}`);
    }
    return value;
}

export function assertMercuryState(value, label = 'page state') {
    const state = assertObject(value, label);
    if (typeof state.url !== 'string' || typeof state.title !== 'string') {
        throw new CommandExecutionError(`Mercury returned malformed ${label}`);
    }
    return state;
}

function assertBooleanFields(state, label, fields) {
    for (const field of fields) {
        if (typeof state[field] !== 'boolean') {
            throw new CommandExecutionError(`Mercury returned malformed ${label}: ${field} must be boolean`);
        }
    }

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check that the Mercury extension/backend is running and the browser session is alive; restart the browser/Mercury if unsure.
  2. Log the raw value returned for the failing label to see what Mercury actually sent.
  3. Update Mercury/extension and CLI to matching versions so payload shapes align.
  4. Retry the command — transient page-state races often resolve on a fresh run.

Example fix

// before
const state = await mercury.getState(); // null when extension unloaded
// after
if (!state) throw new Error('Mercury extension not responding; reload extension and retry');
const checked = assertObject(state, 'page state');
Defensive patterns

Strategy: try-catch

Validate before calling

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

Type guard

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

Try / catch

try {
  const state = await inspectMercury(page);
} catch (e) {
  if (e instanceof CommandExecutionError && e.message.includes('Mercury returned malformed')) {
    console.error('Mercury payload unusable; reloading extension/session and retrying once...');
    await restartMercury();
    return inspectMercury(page);
  }
  throw e;
}

Prevention

When it happens

Trigger: Mercury returns null/undefined where a state object was expected; returns an array instead of an object; a code path returns a string/number (e.g. an error message string) that is fed to assertObject/state.

Common situations: Mercury extension not loaded or crashed so the bridge returns null; page navigated away so the snapshot is empty; version mismatch where Mercury returns a differently-shaped payload; network/timeout yielding an error string instead of JSON.

Understand the failure class

Related errors


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