{"record":{"id":"309a95cf62ec9d1a","repo":"RocketChat/Rocket.Chat","slug":"external-service-response-does-not-match-expected","errorCode":null,"errorMessage":"External service response does not match expected format","messagePattern":"External service response does not match expected format","errorType":"exception","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"apps/meteor/ee/server/api/v1/omnichannel/lib/triggers.ts","lineNumber":40,"sourceCode":"\t\t\t// SECURITY: Integrations can only be configured by users with enough privileges. It's ok to disable this check here.\n\t\t\tignoreSsrfValidation: true,\n\t\t});\n\n\t\tif (!response.ok || response.status !== 200) {\n\t\t\tconst text = await response.text();\n\t\t\tthrow new Error(text);\n\t\t}\n\n\t\tconst data = await response.json();\n\n\t\tconst { contents } = data;\n\n\t\tif (\n\t\t\t!Array.isArray(contents) ||\n\t\t\t!contents.length ||\n\t\t\t!contents.every(({ msg, order }) => typeof msg === 'string' && typeof order === 'number')\n\t\t) {\n\t\t\tthrow new Error('External service response does not match expected format');\n\t\t}\n\n\t\treturn {\n\t\t\tresponse: {\n\t\t\t\tstatusCode: response.status,\n\t\t\t\tcontents: data?.contents || [],\n\t\t\t},\n\t\t};\n\t} catch (error: any) {\n\t\tconst isTimeout = error.message === 'The user aborted a request.';\n\t\treturn {\n\t\t\terror: isTimeout ? 'error-timeout' : 'error-invalid-external-service-response',\n\t\t\tresponse: error.message,\n\t\t\tfallbackMessage,\n\t\t};\n\t}\n}\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/ee/server/api/v1/omnichannel/lib/triggers.ts#L22-L58","documentation":"Inside callTriggerExternalService, a successful HTTP response is parsed as JSON and must carry a top-level contents array that is non-empty and whose every element has msg:string and order:number. Any other shape throws this error; the caller maps it to the API error error-invalid-external-service-response (a fetch abort maps to error-timeout instead). Both livechat/triggers/external-service/test and livechat/triggers/:_id/external-service/call surface it.","triggerScenarios":"Your external service returns HTTP 200 with {}, {\"contents\": []}, {\"messages\":[...]}, or entries like {msg: 'Hi', order: '1'} (order as string) or entries missing msg. The JSON parse itself must also succeed, so the body must be actual JSON.","commonSituations":"Webhook implemented from a different spec or with its own envelope; field named message instead of msg; order serialized as a string; services that return an empty contents array when no agent reply is available.","solutions":["Return exactly { \"contents\": [{ \"msg\": string, \"order\": number }, ...] } with a non-empty array.","Ensure order is a JSON number and msg is present and a string on every entry.","Iterate against POST /api/v1/livechat/triggers/external-service/test (rate-limited to 15 req/min) with your serviceUrl until it returns success.","Add a contract test on the service side asserting this exact shape."],"exampleFix":"// before (external service handler)\nres.status(200).json({ messages: [{ msg: 'Hello', order: '1' }] });\n\n// after\nres.status(200).json({ contents: [{ msg: 'Hello', order: 1 }] });","handlingStrategy":"type-guard","validationCode":"const res = await fetch(serviceUrl, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', 'X-RocketChat-Livechat-Token': token },\n  body: JSON.stringify({ metadata: null, visitorToken: '1234567890' }),\n});\nconst data = await res.json();\nif (!isExternalServicePayload(data)) {\n  throw new Error('Service response must be { contents: [{ msg, order }] }');\n}","typeGuard":"const isExternalServicePayload = (d: unknown): d is { contents: { msg: string; order: number }[] } =>\n  typeof d === 'object' &&\n  d !== null &&\n  Array.isArray((d as { contents?: unknown }).contents) &&\n  (d as { contents: unknown[] }).contents.length > 0 &&\n  (d as { contents: unknown[] }).contents.every(\n    (c) => typeof (c as { msg?: unknown })?.msg === 'string' && typeof (c as { order?: unknown })?.order === 'number',\n  );","tryCatchPattern":"try {\n  const r = await api.post('/v1/livechat/triggers/external-service/test', { webhookUrl });\n} catch (e) {\n  if (e?.response?.data?.error === 'error-invalid-external-service-response') {\n    // fix the service to return { contents: [{ msg, order }] }, then re-test\n  } else if (e?.response?.data?.error === 'error-timeout') {\n    // service too slow: raise timeout or optimize the service\n  } else throw e;\n}","preventionTips":["Contract-test the external service against the exact contents/msg/order shape, including numeric order.","Never wrap the payload in a custom envelope.","Use the rate-limited /test endpoint to iterate before wiring the trigger."],"tags":["omnichannel","livechat-triggers","external-service","webhook","response-schema"],"backgroundTag":"schema-validation-failed","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}