RocketChat/Rocket.Chat · error · Error
The "customFields" query parameter must be a valid JSON.
Error message
The "customFields" query parameter must be a valid JSON.
What it means
Thrown by GET /api/v1/livechat/rooms when the optional `customFields` query parameter cannot be parsed as JSON, or parses to something that is not a plain object (an array, null, a string, a number). The endpoint runs JSON.parse(customFields) and also re-raises this same message when the parsed value fails the object shape check, so the error text always points at 'valid JSON' even when the real problem was that you sent an array or `null`.
Solutions
- Send the value as a URL-encoded flat JSON object of string keys to string values: customFields=%7B%22advisor%22%3A%22c82b87e0%22%7D
- Always build the query string with encodeURIComponent(JSON.stringify(filter)) in your client
- If you want all rooms with any custom fields, drop the parameter entirely instead of sending {} or null
- Verify with a quick client-side JSON.parse plus Object.keys check before firing the request
Example fix
// before
const url = `${server}/api/v1/livechat/rooms?customFields={"advisor":"c82b87e0"}`;
// after
const url = `${server}/api/v1/livechat/rooms?customFields=${encodeURIComponent(JSON.stringify({ advisor: 'c82b87e0' }))}`; Defensive patterns
Strategy: validation
Validate before calling
const customFields = { advisor: 'c82b87e0' };
const encoded = encodeURIComponent(JSON.stringify(customFields));
// only values JSON.parse can read AND that are plain objects survive the server check
await fetch(`${server}/api/v1/livechat/rooms?customFields=${encoded}`, { headers }); Type guard
const isPlainStringMap = (v: unknown): v is Record<string, string> =>
typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === 'string');
function assertCustomFieldsParam(raw: string): Record<string, string> {
const parsed: unknown = JSON.parse(raw); // throws locally instead of a 400 remotely
if (!isPlainStringMap(parsed)) throw new TypeError('customFields must be a JSON object of string -> string');
return parsed;
} Try / catch
try {
const res = await fetch(url);
const body = await res.json();
if (!body.success && /must be a valid JSON/.test(body.error)) {
// fix encoding of customFields and retry once
}
} catch (e) { /* network errors only; param errors arrive as 400 bodies */ } Prevention
- Always encodeURIComponent(JSON.stringify(obj)) query params that carry JSON
- Keep a client-side unit test that round-trips every query param through URL parsing
- Never hand-concatenate JSON into URLs; use the URL / URLSearchParams APIs
When it happens
Trigger: Calling GET /api/v1/livechat/rooms?customFields=... with: unquoted keys ({advisor:c82b87e0}), a JSON array ([{"advisor":"x"}]), the literal string null, a truncated payload caused by not URL-encoding (the & or " characters cut the query string), or double-encoded JSON ({\"advisor\":\"x\"}).
Common situations: Building the URL by string concatenation without encodeURIComponent so {"a":"b"} is clipped at the first encoded character; copying a Mongo filter that wraps fields in an array; testing with curl and losing quotes to shell interpolation; values that contain & (e.g. session ids) splitting the query string.
Related errors
- error-invalid-params-custom
- error-token-param-not-provided
- invalid-chart-name
- Invalid custom fields
- message-length-exceeds-character-limit
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/903a954f04510b0a.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/omnichannel/rooms.ts:56
const hasAdminAccess = await hasPermissionAsync(this.user, 'view-livechat-rooms');
const hasAgentAccess = (await hasPermissionAsync(this.user, 'view-l-room')) && agents?.includes(this.userId) && agents?.length === 1;
if (!hasAdminAccess && !hasAgentAccess) {
return API.v1.forbidden();
}
let parsedCf: { [key: string]: string } | undefined = undefined;
if (customFields) {
try {
const parsedCustomFields = JSON.parse(customFields) as { [key: string]: string };
if (typeof parsedCustomFields !== 'object' || Array.isArray(parsedCustomFields) || parsedCustomFields === null) {
throw new Error('Invalid custom fields');
}
// Model's already checking for the keys, so we don't need to do it here.
parsedCf = parsedCustomFields;
} catch (e) {
throw new Error('The "customFields" query parameter must be a valid JSON.');
}
}
return API.v1.success(
await findRooms({
agents,
roomName,
departmentId,
...(isBoolean(open) && { open: open === true || open === 'true' }),
createdAt: createdAtParam,
closedAt: closedAtParam,
tags,
customFields: parsedCf,
onhold,
queued,
units,
query,
options: { offset, count, sort, fields },View on GitHub (pinned to b2c16d5842)