{"record":{"id":"302ec48339ace937","repo":"RocketChat/Rocket.Chat","slug":"query-must-be-an-object","errorCode":null,"errorMessage":"query must be an object","messagePattern":"query must be an object","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/lib/isValidQuery.ts","lineNumber":15,"sourceCode":"import { isRecord } from '@rocket.chat/tools';\n\nimport { removeDangerousProps } from './cleanQuery';\n\ntype Query = { [k: string]: any };\n\nexport const isValidQuery: {\n\t(query: Query, allowedAttributes: string[], allowedOperations: string[]): boolean;\n\terrors: string[];\n} = Object.assign(\n\t(query: Query, allowedAttributes: string[], allowedOperations: string[]): boolean => {\n\t\tisValidQuery.errors = [];\n\t\t// query is an object with null prototype, so it wont be instance of Object\n\t\tif (!isRecord(query)) {\n\t\t\tthrow new Error('query must be an object');\n\t\t}\n\n\t\treturn verifyQuery(query, allowedAttributes, allowedOperations);\n\t},\n\t{\n\t\terrors: [],\n\t},\n);\n\nconst verifyQuery = (query: Query, allowedAttributes: string[], allowedOperations: string[], parent = ''): boolean => {\n\treturn Object.entries(removeDangerousProps({ ...query })).every(([key, value]) => {\n\t\tconst path = parent ? `${parent}.${key}` : key;\n\t\tif (key.startsWith('$')) {\n\t\t\tif (!allowedOperations.includes(key)) {\n\t\t\t\tisValidQuery.errors.push(`Invalid operation: ${key}`);\n\t\t\t\treturn false;\n\t\t\t}\n","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/lib/isValidQuery.ts#L1-L33","documentation":"isValidQuery (apps/meteor/server/api/lib/isValidQuery.ts) validates query/selector objects against an allowlist of attributes and operators. Before validating anything it asserts the input is a record: isRecord(query). If the value is an array, string, number, boolean or null it throws this plain Error. The isRecord check (rather than instanceof Object) exists because EJSON-parsed and null-prototype objects fail instanceof; conversely, arrays are deliberately rejected.","triggerScenarios":"users.selector endpoint (apps/meteor/server/api/v1/users.ts:1702) with a selector whose conditions field is a JSON array or string (e.g. selector={\"term\":\"a\",\"conditions\":[...]}); or any parseJsonQuery-backed endpoint with ALLOW_UNSAFE_QUERY_AND_FIELDS_API_PARAMS=TRUE where the query param parses to a JSON array or scalar (query=%5B1,2%5D, query=%22foo%22).","commonSituations":"Building a $and/$or filter as an array and assigning it directly to conditions instead of wrapping it ({\"$and\": [...]}) ; passing a pre-stringified selector inside another JSON string so it double-parses into a string; client code that conditionally sends [] instead of {} for empty filters.","solutions":["Make the validated value a JSON object — wrap arrays under an operator key: {\"conditions\": {\"$and\": [...]}}","Default to {} (empty object) instead of [] or null when there are no filters","JSON.stringify the whole selector once, not a string inside a string, before sending the query param"],"exampleFix":"// before\nGET /api/v1/users.selector?selector={\"term\":\"a\",\"conditions\":[{\"name\":\"x\"}]}\n\n// after\nGET /api/v1/users.selector?selector={\"term\":\"a\",\"conditions\":{\"name\":\"x\"}}\n// array filters go under an operator: {\"conditions\":{\"$and\":[{\"name\":\"x\"}]}}","handlingStrategy":"type-guard","validationCode":"const toConditions = (v: unknown): Record<string, unknown> =>\n  (typeof v === 'object' && v !== null && !Array.isArray(v)) ? v as Record<string, unknown> : {}; // never send [] or strings","typeGuard":"const isRecord = (v: unknown): v is Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v);\n\n// before calling users.selector or building a query param:\nif (!isRecord(selector.conditions)) selector.conditions = {};","tryCatchPattern":"try {\n  await client.get('/api/v1/users.selector', { params: { selector: JSON.stringify(selector) } });\n} catch (e: any) {\n  if ((e?.response?.data?.error ?? '').includes('query must be an object')) {\n    throw new ValidationError('conditions must be a JSON object, not an array/string');\n  }\n  throw e;\n}","preventionTips":["Always wrap array filters under an operator key ({\"$and\": [...]}) instead of passing the bare array","Default filters to {} not [] or null","Stringify the selector exactly once; double-stringified payloads parse back into strings"],"tags":["rest-api","query-validation","invalid-param","plain-error"],"backgroundTag":"query-validation-failed","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}