{"record":{"id":"903a954f04510b0a","repo":"RocketChat/Rocket.Chat","slug":"the-customfields-query-parameter-must-be-a-valid","errorCode":null,"errorMessage":"The \"customFields\" query parameter must be a valid JSON.","messagePattern":"The \"customFields\" query parameter must be a valid JSON\\.","errorType":"exception","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/v1/omnichannel/rooms.ts","lineNumber":56,"sourceCode":"\n\t\t\tconst hasAdminAccess = await hasPermissionAsync(this.user, 'view-livechat-rooms');\n\t\t\tconst hasAgentAccess = (await hasPermissionAsync(this.user, 'view-l-room')) && agents?.includes(this.userId) && agents?.length === 1;\n\t\t\tif (!hasAdminAccess && !hasAgentAccess) {\n\t\t\t\treturn API.v1.forbidden();\n\t\t\t}\n\n\t\t\tlet parsedCf: { [key: string]: string } | undefined = undefined;\n\t\t\tif (customFields) {\n\t\t\t\ttry {\n\t\t\t\t\tconst parsedCustomFields = JSON.parse(customFields) as { [key: string]: string };\n\t\t\t\t\tif (typeof parsedCustomFields !== 'object' || Array.isArray(parsedCustomFields) || parsedCustomFields === null) {\n\t\t\t\t\t\tthrow new Error('Invalid custom fields');\n\t\t\t\t\t}\n\n\t\t\t\t\t// Model's already checking for the keys, so we don't need to do it here.\n\t\t\t\t\tparsedCf = parsedCustomFields;\n\t\t\t\t} catch (e) {\n\t\t\t\t\tthrow new Error('The \"customFields\" query parameter must be a valid JSON.');\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn API.v1.success(\n\t\t\t\tawait findRooms({\n\t\t\t\t\tagents,\n\t\t\t\t\troomName,\n\t\t\t\t\tdepartmentId,\n\t\t\t\t\t...(isBoolean(open) && { open: open === true || open === 'true' }),\n\t\t\t\t\tcreatedAt: createdAtParam,\n\t\t\t\t\tclosedAt: closedAtParam,\n\t\t\t\t\ttags,\n\t\t\t\t\tcustomFields: parsedCf,\n\t\t\t\t\tonhold,\n\t\t\t\t\tqueued,\n\t\t\t\t\tunits,\n\t\t\t\t\tquery,\n\t\t\t\t\toptions: { offset, count, sort, fields },","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/v1/omnichannel/rooms.ts#L38-L74","documentation":"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`.","triggerScenarios":"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\\\"}).","commonSituations":"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.","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"],"exampleFix":"// before\nconst url = `${server}/api/v1/livechat/rooms?customFields={\"advisor\":\"c82b87e0\"}`;\n// after\nconst url = `${server}/api/v1/livechat/rooms?customFields=${encodeURIComponent(JSON.stringify({ advisor: 'c82b87e0' }))}`;","handlingStrategy":"validation","validationCode":"const customFields = { advisor: 'c82b87e0' };\nconst encoded = encodeURIComponent(JSON.stringify(customFields));\n// only values JSON.parse can read AND that are plain objects survive the server check\nawait fetch(`${server}/api/v1/livechat/rooms?customFields=${encoded}`, { headers });","typeGuard":"const isPlainStringMap = (v: unknown): v is Record<string, string> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === 'string');\n\nfunction assertCustomFieldsParam(raw: string): Record<string, string> {\n  const parsed: unknown = JSON.parse(raw); // throws locally instead of a 400 remotely\n  if (!isPlainStringMap(parsed)) throw new TypeError('customFields must be a JSON object of string -> string');\n  return parsed;\n}","tryCatchPattern":"try {\n  const res = await fetch(url);\n  const body = await res.json();\n  if (!body.success && /must be a valid JSON/.test(body.error)) {\n    // fix encoding of customFields and retry once\n  }\n} catch (e) { /* network errors only; param errors arrive as 400 bodies */ }","preventionTips":["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"],"tags":["rest-api","livechat","omnichannel","query-params","json","validation"],"backgroundTag":"malformed-json-parameter","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}