{"record":{"id":"cc354bbf80cd6ba4","repo":"koala73/worldmonitor","slug":"malformed-command-expected-an-array-whose-first-element-is","errorCode":null,"errorMessage":"Malformed command: expected an array whose first element is the command name","messagePattern":"Malformed command: expected an array whose first element is the command name","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"docker/redis-rest-proxy.mjs","lineNumber":689,"sourceCode":"// decision and nothing else. Without it a caller can only catch every throw\n// from this function, and `String(args[0])` throws a TypeError on a null or\n// undefined body element — so a malformed request comes back as an\n// authorization failure, which is precisely the misdirection #8265 was. Tagged\n// on the error object rather than raised as a named subclass because\n// tests/redis-rest-proxy-command-parity.test.mjs extracts this function's\n// source and evals it standalone; a class declared elsewhere in this file\n// would be undefined there.\nfunction assertCommandAllowed(args) {\n  // Shape first, authorization second. String(args[0]) turns a missing verb\n  // into \"undefined\" and a null one into \"null\", and the allowlist then\n  // refuses those as if they were commands — so `[[]]` and `[[null]]` came\n  // back as 403 \"Command not allowed: UNDEFINED\"/\"NULL\" while a null ELEMENT\n  // (which throws before this line) came back as 500. Same malformed body,\n  // two status classes, two of them in the authorization channel. A\n  // well-formed command that is simply not allowed is the only thing past\n  // this point.\n  if (!Array.isArray(args) || typeof args[0] !== 'string' || args[0].trim() === '') {\n    throw new TypeError('Malformed command: expected an array whose first element is the command name');\n  }\n  const cmd = args[0].toUpperCase();\n  if (cmd === 'EVAL') {\n    if (!isAllowedEval(args)) {\n      console.error('Command not allowed: EVAL (script not in the pinned allowlist)');\n      throw Object.assign(new Error('Command not allowed: EVAL (script not in the pinned allowlist)'), { commandNotAllowed: true });\n    }\n  } else if (!ALLOWED_COMMANDS.has(cmd)) {\n    console.error(`Command not allowed: ${cmd}`);\n    throw Object.assign(new Error(`Command not allowed: ${cmd}`), { commandNotAllowed: true });\n  }\n  return cmd;\n}\n\nfunction commandForExecution(args) {\n  const cmd = assertCommandAllowed(args);\n  const command = [cmd, ...args.slice(1).map(String)];\n  if (cmd === 'EVAL') {","sourceCodeStart":671,"sourceCodeEnd":707,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/docker/redis-rest-proxy.mjs#L671-L707","documentation":"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.","triggerScenarios":"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.","commonSituations":"Client libraries sending object-wrapped commands instead of arrays; empty POST bodies; double-encoded JSON strings; curl tests with wrong payload shape.","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']))."],"exampleFix":"// before\nfetch(proxy, { method: 'POST', body: JSON.stringify({ command: 'GET', key: 'foo' }) });\n// after\nfetch(proxy, { method: 'POST', body: JSON.stringify(['GET', 'foo']) });","handlingStrategy":"validation","validationCode":"const args = JSON.parse(rawBody);\nif (!Array.isArray(args) || typeof args[0] !== 'string' || args[0].trim() === '') throw new Error('command must be [\"CMD\", ...args]');","typeGuard":"const isCommand = (v) => Array.isArray(v) && v.length > 0 && typeof v[0] === 'string' && v[0].trim() !== '';","tryCatchPattern":"try { await proxyCommand(args); } catch (e) { if (String(e.message).startsWith('Malformed command')) { log('bad payload shape', args); return 400; } throw e; }","preventionTips":["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"],"tags":["redis","http","input-validation","proxy"],"backgroundTag":"invalid-argument-format","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}