RocketChat/Rocket.Chat · error · Error

query must be an object

Error message

query must be an object

What it means

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.

Source

Thrown at apps/meteor/server/api/lib/isValidQuery.ts:15

import { isRecord } from '@rocket.chat/tools';

import { removeDangerousProps } from './cleanQuery';

type Query = { [k: string]: any };

export const isValidQuery: {
	(query: Query, allowedAttributes: string[], allowedOperations: string[]): boolean;
	errors: string[];
} = Object.assign(
	(query: Query, allowedAttributes: string[], allowedOperations: string[]): boolean => {
		isValidQuery.errors = [];
		// query is an object with null prototype, so it wont be instance of Object
		if (!isRecord(query)) {
			throw new Error('query must be an object');
		}

		return verifyQuery(query, allowedAttributes, allowedOperations);
	},
	{
		errors: [],
	},
);

const verifyQuery = (query: Query, allowedAttributes: string[], allowedOperations: string[], parent = ''): boolean => {
	return Object.entries(removeDangerousProps({ ...query })).every(([key, value]) => {
		const path = parent ? `${parent}.${key}` : key;
		if (key.startsWith('$')) {
			if (!allowedOperations.includes(key)) {
				isValidQuery.errors.push(`Invalid operation: ${key}`);
				return false;
			}

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Make the validated value a JSON object — wrap arrays under an operator key: {"conditions": {"$and": [...]}}
  2. Default to {} (empty object) instead of [] or null when there are no filters
  3. JSON.stringify the whole selector once, not a string inside a string, before sending the query param

Example fix

// before
GET /api/v1/users.selector?selector={"term":"a","conditions":[{"name":"x"}]}

// after
GET /api/v1/users.selector?selector={"term":"a","conditions":{"name":"x"}}
// array filters go under an operator: {"conditions":{"$and":[{"name":"x"}]}}
Defensive patterns

Strategy: type-guard

Validate before calling

const toConditions = (v: unknown): Record<string, unknown> =>
  (typeof v === 'object' && v !== null && !Array.isArray(v)) ? v as Record<string, unknown> : {}; // never send [] or strings

Type guard

const isRecord = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v);

// before calling users.selector or building a query param:
if (!isRecord(selector.conditions)) selector.conditions = {};

Try / catch

try {
  await client.get('/api/v1/users.selector', { params: { selector: JSON.stringify(selector) } });
} catch (e: any) {
  if ((e?.response?.data?.error ?? '').includes('query must be an object')) {
    throw new ValidationError('conditions must be a JSON object, not an array/string');
  }
  throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/302ec48339ace937. Report an issue: GitHub.