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
- Read the joined isValidQuery errors in the response body — they name the exact disallowed field/operator
- Restrict operators to $or/$and/$regex/$options and use regex strings instead of $in/$nin/$gt
- Ensure every key in query appears in the fields projection (e.g. add "fields":{"username":1} when querying username)
- 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
- Derive query keys from the same fields projection you send
- Never paste raw mongo shell queries into the query param
- URL-encode JSON.stringify(query) exactly once
- Keep a unit test fixture of known-good query strings per endpoint
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
- error-invalid-user
- error-user-param-not-provided
- error-action-not-allowed
- error-bio-size-exceeded
- error-blocked-username
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)