{"record":{"id":"d4cd7321b2e8a25d","repo":"RocketChat/Rocket.Chat","slug":"invalid-custom-fields","errorCode":null,"errorMessage":"Invalid custom fields","messagePattern":"Invalid custom fields","errorType":"exception","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/server/api/v1/omnichannel/rooms.ts","lineNumber":50,"sourceCode":"\t\t\tconst { sort, fields, query } = await this.parseJsonQuery();\n\t\t\tconst { agents, departmentId, open, tags, roomName, onhold, queued, units } = this.queryParams;\n\t\t\tconst { createdAt, customFields, closedAt } = this.queryParams;\n\n\t\t\tconst createdAtParam = validateDateParams('createdAt', createdAt);\n\t\t\tconst closedAtParam = validateDateParams('closedAt', closedAt);\n\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,","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/server/api/v1/omnichannel/rooms.ts#L32-L68","documentation":"An internal guard inside GET /api/v1/livechat/rooms: after JSON.parse of the customFields query parameter, it fires when the parsed value is not a plain object (typeof !== 'object', an array, or null). Important nuance: this throw is immediately swallowed by the surrounding try/catch and re-thrown as 'The \"customFields\" query parameter must be a valid JSON.' — so clients never see the 'Invalid custom fields' text; every malformed shape (bad JSON or non-object JSON) surfaces as the JSON message.","triggerScenarios":"Sending customFields=%5B%7B%7D%5D (a JSON array), customFields=null, customFields=%22abc%22 (a JSON string), or unparseable JSON — all end in the same 400 with the 'must be a valid JSON' message; passing an object whose keys are unknown to the LivechatCustomField model is NOT an error here (the model layer validates keys).","commonSituations":"Integrations sending an array of field objects; double-encoded JSON (a string containing JSON); forgetting encodeURIComponent so '&' inside the JSON truncates it; passing 'null' literally for 'no filter'.","solutions":["Send a plain object: customFields=<urlencoded JSON.stringify({ key: 'value' })>","When no filtering is desired, omit the customFields parameter entirely instead of sending null/[]","Encode correctly: encodeURIComponent(JSON.stringify(fields))"],"exampleFix":"// before\nconst q = `customFields=${JSON.stringify([{ key: 'region', value: 'emea' }])}`; // array → rejected\n\n// after\nconst q = `customFields=${encodeURIComponent(JSON.stringify({ region: 'emea' }))}`; // plain object","handlingStrategy":"validation","validationCode":"function toCustomFieldsParam(fields) {\n  if (!fields || typeof fields !== 'object' || Array.isArray(fields)) return ''; // omit instead of sending bad shapes\n  return `customFields=${encodeURIComponent(JSON.stringify(fields))}`;\n}","typeGuard":"function isPlainObject(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try { await listRooms(query); } catch (e) { if (e.message?.includes('customFields')) { retryWithoutCustomFields(); return; } throw e; }","preventionTips":["Send customFields only as a plain JSON object, URL-encoded","Omit the parameter entirely when not filtering (never send 'null' or '[]')","Remember both bad JSON and non-object JSON surface as the same 'valid JSON' error"],"tags":["omnichannel","livechat","query-params","json-parsing","rest-api"],"backgroundTag":"json-parse-error","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}