RocketChat/Rocket.Chat · error · Meteor.Error

error-invalid-query

error-invalid-query

Error message

isValidQuery.errors.join('\n')

What it means

Thrown by GET /api/v1/users.list when the `query` query-param (parsed by parseJsonQuery) fails isValidQuery validation. The server only permits filtering on field keys derived from the projection (plus aliases like emails.address.*) and only the operators $or/$and/$regex/$options. The message is isValidQuery.errors.join('\n'), so it lists every offending field or operator.

Solutions

  1. Read the joined isValidQuery errors in the response body — they name the exact disallowed field/operator
  2. Restrict operators to $or/$and/$regex/$options and use regex strings instead of $in/$nin/$gt
  3. Ensure every key in query appears in the fields projection (e.g. add "fields":{"username":1} when querying username)
  4. Test the exact URL-encoded JSON of the query param with curl before wiring it into code

Example fix

// before
GET /api/v1/users.list?query={"emails.address":{"$in":["a@b.c"]}}
// after
GET /api/v1/users.list?fields={"emails":1}&query={"emails.address":{"$regex":"^a@b.c$","$options":"i"}}
Defensive patterns

Strategy: validation

Validate before calling

const allowedOps = new Set(['$or','$and','$regex','$options']);
const q = JSON.parse(rawQuery);
const ok = Object.entries(q).every(([k,v]) => (k.startsWith('$') ? allowedOps.has(k) : projectionKeys.includes(k)) && (v == null || typeof v !== 'object' || Object.keys(v).every((op) => op.startsWith('$') ? allowedOps.has(op) : true)));
if (!ok) throw new Error('query uses disallowed field/operator');

Type guard

const isAllowedQuery = (q: unknown, fields: string[]): q is Record<string, unknown> =>
  typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).every((k) => fields.includes(k) || ['$or','$and'].includes(k));

Try / catch

try { await sdk.get('users.list', { query: rawQuery }); } catch (e) { if (e.response?.data?.errorType === 'error-invalid-query') { console.error(e.response.data.details ?? e.response.data.message); /* strip bad ops, retry once */ } else throw e; }

Prevention

When it happens

Trigger: Calling users.list with ?query={"emails.address":{"$in":[...]}} (operator $in not allowed), ?query={"roles":"admin"} (roles not in the allowed field list for the projection), or a non-object/misspelled query value. Also triggered when `fields` projection keys and query keys are inconsistent, since allowed query fields are derived from inclusiveFieldsKeys.

Common situations: Clients copy a MongoDB query that works in mongo shell into the REST query param; scripts written against older Rocket.Chat versions that accepted arbitrary operators; adding customFields to the query without including customFields in the fields projection.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@e4b8178b20 (2026-08-21). Data as JSON: /api/errors/0ff3a8034c44cd92. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/server/api/v1/users.ts:719

			// if user provided a query, validate it with their allowed operators
			// otherwise we use the default query (with $regex and $options)
			if (
				!isValidQuery(
					nonEmptyQuery,
					[
						...inclusiveFieldsKeys,
						inclusiveFieldsKeys.includes('emails') && 'emails.address.*',
						inclusiveFieldsKeys.includes('username') && 'username.*',
						inclusiveFieldsKeys.includes('name') && 'name.*',
						inclusiveFieldsKeys.includes('type') && 'type.*',
						inclusiveFieldsKeys.includes('customFields') && 'customFields.*',
					].filter(Boolean) as string[],
					// At this point, we have already validated the user query not containing malicious fields
					// On here we are using our own query so we can allow some extra fields
					[...this.queryOperations, '$regex', '$options'],
				)
			) {
				throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n'));
			}

			const hidden = await getUsersHiddenFrom(this.userId);

			if (hidden && queryFiltersStatus(query)) {
				nonEmptyQuery.$and = [...(nonEmptyQuery.$and ?? []), { _id: { $nin: [...hidden] } }];
			}

			const actualSort = sort || { username: 1 };

			if (sort?.status) {
				actualSort.active = sort.status;
			}

			if (sort?.name) {
				actualSort.nameInsensitive = sort.name;
			}

View on GitHub (pinned to e4b8178b20)