tinyhumansai/openhuman · error

${context} returned an invalid response shape

Error message

${context} returned an invalid response shape

What it means

expectArray unwrapped the payload (first stripping a CLI envelope { result, logs } if present) and the value was not an Array. The context string names the operation, e.g. 'Channel definitions', 'Channel status', 'Discord guild list', 'Discord channel list'. It marks a contract violation between the core/CLI and this client.

Source

Thrown at app/src/services/api/channelConnectionsApi.ts:62

function asRecord(value: unknown): Record<string, unknown> | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }
  return value as Record<string, unknown>;
}

function unwrapCliEnvelope<T>(payload: unknown): T {
  const record = asRecord(payload);
  if (record && 'result' in record && 'logs' in record && Array.isArray(record.logs)) {
    return record.result as T;
  }
  return payload as T;
}

function expectArray<T>(payload: unknown, context: string): T[] {
  const unwrapped = unwrapCliEnvelope<unknown>(payload);
  if (!Array.isArray(unwrapped)) {
    throw new Error(`${context} returned an invalid response shape`);
  }
  return unwrapped as T[];
}

function expectObject<T extends object>(payload: unknown, context: string): T {
  const unwrapped = unwrapCliEnvelope<unknown>(payload);
  const record = asRecord(unwrapped);
  if (!record) {
    throw new Error(`${context} returned an invalid response shape`);
  }
  return record as T;
}

function expectDiscordLinkStart(payload: unknown): DiscordLinkStartResult {
  const record = expectObject<Record<string, unknown>>(payload, 'Discord link start');
  if (typeof record.linkToken !== 'string' || !record.linkToken) {
    throw new Error('Discord link start response missing required string field: linkToken');
  }

View on GitHub (pinned to a221052e0d)

Solutions

  1. Log the raw payload at the failure site and compare it with the core handler's actual return shape for the named operation
  2. Restart/update the core so its response shape matches what this client version expects
  3. If you control the mock/test server, return the unwrapped array (or { result: [...], logs: [] })
  4. If the core legitimately changed, update this client's unwrapping/expectation to the new contract

Example fix

// before (mock/test server route)
app.get('/rpc', () => jsonResponse({ channels: [/* ... */] }));

// after
app.get('/rpc', () => jsonResponse([/* ... */]));
// or a CLI-shaped envelope the helper understands:
app.get('/rpc', () => jsonResponse({ result: [/* ... */], logs: [] }));
Defensive patterns

Strategy: type-guard

Validate before calling

const raw = await callCoreRpc<unknown>(params);
const isUsableArray = Array.isArray(unwrapCliEnvelope(raw));
if (!isUsableArray) logPayloadForContractDiff(raw);

Type guard

const isArrayOf = <T>(v: unknown): v is T[] => Array.isArray(unwrapCliEnvelope(v));

function unwrapCliEnvelope<T>(payload: unknown): T {
  const record = typeof payload === 'object' && payload !== null ? payload as Record<string, unknown> : null;
  if (record && 'result' in record && 'logs' in record && Array.isArray(record.logs)) {
    return record.result as T;
  }
  return payload as T;
}

Try / catch

try { const defs = await channelConnectionsApi.listChannels(); }
catch (e) {
  if (String((e as Error).message).includes('invalid response shape')) {
    logPayloadAndCoreVersion(); showError('Channel data unavailable — restart the app and retry.');
  } else throw e;
}

Prevention

When it happens

Trigger: channelConnectionsApi.listChannels()/status()/listDiscordGuilds()/listDiscordChannels() receiving an object or string instead of an array — e.g. an error envelope { error: ... } passed through as success, a handler change returning { channels: [...] } instead of a bare array, or a legacy CLI path emitting a scalar.

Common situations: Version skew after a core-side response refactor; a CLI-in-the-middle returning its envelope in a shape unwrapCliEnvelope doesn't recognize ({ result, logs } with logs non-array bypasses the unwrap); mocks in tests returning wrapped objects; error payloads not surfaced as exceptions upstream.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/435d79271eef5125. Report an issue: GitHub.