koala73/worldmonitor · error · TypeError
Malformed command: expected an array whose first element is…
Error message
Malformed command: expected an array whose first element is the command name
What it means
assertCommandAllowed in the Redis REST proxy first checks that the request body is a well-formed command: an array whose first element is a non-empty string command name. Anything else (missing array, null body, non-string first element, empty name) throws a TypeError rendered as a 400 'Malformed command' instead of the 403 'Command not allowed' authorization channel. This separation keeps authorization failures distinct from malformed input.
Solutions
- Send the command as a JSON array whose first element is the command name, e.g. ["GET", "key"].
- Ensure the Content-Type is application/json and the body parses to an array, not an object or string.
- Do not send an empty command name; use the real Redis command as element 0.
- Fix client code to serialize commands as arrays (e.g. JSON.stringify(['SET','k','v'])).
Example fix
// before
fetch(proxy, { method: 'POST', body: JSON.stringify({ command: 'GET', key: 'foo' }) });
// after
fetch(proxy, { method: 'POST', body: JSON.stringify(['GET', 'foo']) }); Defensive patterns
Strategy: validation
Validate before calling
const args = JSON.parse(rawBody);
if (!Array.isArray(args) || typeof args[0] !== 'string' || args[0].trim() === '') throw new Error('command must be ["CMD", ...args]'); Type guard
const isCommand = (v) => Array.isArray(v) && v.length > 0 && typeof v[0] === 'string' && v[0].trim() !== '';
Try / catch
try { await proxyCommand(args); } catch (e) { if (String(e.message).startsWith('Malformed command')) { log('bad payload shape', args); return 400; } throw e; } Prevention
- Always serialize Redis commands as JSON arrays with the command name first
- Add a client-side helper that builds and validates the array shape
- Use application/json content type and avoid double-encoding
- Unit-test the proxy client against a shared payload-shape fixture
When it happens
Trigger: POSTing to the proxy with a JSON body that is not an array (e.g. `{"cmd":"GET"}`), an array whose first element is null/a number, or `[""]`; also sending a body that fails to decode into the expected array shape.
Common situations: Client libraries sending object-wrapped commands instead of arrays; empty POST bodies; double-encoded JSON strings; curl tests with wrong payload shape.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ${err.message}
- Redis HTTP
- Redis transaction failed: HTTP
- Widget body too large
- Airport delay filters are not supported
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/cc354bbf80cd6ba4.
Report an issue: GitHub.
Appendix: source
Thrown at docker/redis-rest-proxy.mjs:689
// decision and nothing else. Without it a caller can only catch every throw
// from this function, and `String(args[0])` throws a TypeError on a null or
// undefined body element — so a malformed request comes back as an
// authorization failure, which is precisely the misdirection #8265 was. Tagged
// on the error object rather than raised as a named subclass because
// tests/redis-rest-proxy-command-parity.test.mjs extracts this function's
// source and evals it standalone; a class declared elsewhere in this file
// would be undefined there.
function assertCommandAllowed(args) {
// Shape first, authorization second. String(args[0]) turns a missing verb
// into "undefined" and a null one into "null", and the allowlist then
// refuses those as if they were commands — so `[[]]` and `[[null]]` came
// back as 403 "Command not allowed: UNDEFINED"/"NULL" while a null ELEMENT
// (which throws before this line) came back as 500. Same malformed body,
// two status classes, two of them in the authorization channel. A
// well-formed command that is simply not allowed is the only thing past
// this point.
if (!Array.isArray(args) || typeof args[0] !== 'string' || args[0].trim() === '') {
throw new TypeError('Malformed command: expected an array whose first element is the command name');
}
const cmd = args[0].toUpperCase();
if (cmd === 'EVAL') {
if (!isAllowedEval(args)) {
console.error('Command not allowed: EVAL (script not in the pinned allowlist)');
throw Object.assign(new Error('Command not allowed: EVAL (script not in the pinned allowlist)'), { commandNotAllowed: true });
}
} else if (!ALLOWED_COMMANDS.has(cmd)) {
console.error(`Command not allowed: ${cmd}`);
throw Object.assign(new Error(`Command not allowed: ${cmd}`), { commandNotAllowed: true });
}
return cmd;
}
function commandForExecution(args) {
const cmd = assertCommandAllowed(args);
const command = [cmd, ...args.slice(1).map(String)];
if (cmd === 'EVAL') {View on GitHub (pinned to e586b8b4b8)