RocketChat/Rocket.Chat · warning

Attachments should be Array, ignoring value

Error message

Attachments should be Array, ignoring value

What it means

processWebhookMessage is the shared handler for incoming webhooks and programmatic message sends. If messageObj.attachments is present but is not an Array, it logs this warning and sets attachments to undefined, so the message still sends — just without attachments. It is input-shape tolerance, not a delivery failure.

Source

Thrown at apps/meteor/server/lib/messages/processWebhookMessage.ts:160

): Promise<WebhookSuccessItem[]>;

export async function processWebhookMessage(
	messageObj: Payload & {
		/**
		 * If true, the response will be sent separately for each channel. Messages will be sent to other channels even if one or more fails. If false or not provided, messages would not be sent to any channel if one or more fails.
		 */
		separateResponse?: boolean;
	},
	user: RequiredField<IUser, 'username'>,
	defaultValues: DefaultValues = { channel: '', alias: '', avatar: '', emoji: '' },
) {
	const rooms: ({ channel: string } & ({ room: IRoom } | { room: IRoom | null; error?: any }))[] = [];
	const sentData: WebhookResponseItem[] = [];

	const channels: Array<string> = [...new Set(ensureArray(messageObj.channel || messageObj.roomId || defaultValues.channel))];

	if (messageObj.attachments && !Array.isArray(messageObj.attachments)) {
		SystemLogger.warn({
			msg: 'Attachments should be Array, ignoring value',
			attachments: messageObj.attachments,
		});
		messageObj.attachments = undefined;
	}

	const message = buildMessage(messageObj, defaultValues);

	for (const channel of channels) {
		const channelType = channel[0];
		const channelValue = channel.slice(1);
		let room: IRoom | null = null;
		try {
			room = await getRoomWithOptionToJoin(channelType, channelValue, user);
			if (!room) {
				throw new Error('error-invalid-room');
			}
			await validateRoomMessagePermissionsAsync(room, { uid: user._id, ...user });

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Wrap the attachment in an array: "attachments": [ { ... } ] instead of "attachments": { ... }
  2. Send real JSON with Content-Type: application/json so the body parses to objects, not strings
  3. Check for stringified JSON: the value must not be a quoted string containing JSON
  4. Validate the payload against the webhook docs (or a JSON schema) before shipping the integration

Example fix

// before
{
  "text": "deploy done",
  "attachments": { "title": "build #42", "color": "#00ff00" }
}

// after
{
  "text": "deploy done",
  "attachments": [ { "title": "build #42", "color": "#00ff00" } ]
}
Defensive patterns

Strategy: type-guard

Validate before calling

const raw = await request.json();
if (raw.attachments !== undefined && !Array.isArray(raw.attachments)) {
  return badRequest('"attachments" must be an array of attachment objects');
}

Type guard

const isAttachmentArray = (value: unknown): value is IMessageAttachment[] => Array.isArray(value);

Prevention

When it happens

Trigger: POSTing to an incoming webhook (/hooks/<id>) with 'attachments' as a plain object or a string — e.g. a single attachment object instead of a one-element array, or stringified JSON ('attachments': '[{...}]') sent to a text/plain content type.

Common situations: Integration scripts copy-pasting a single attachment object from docs without wrapping it in []; JSON payloads built by string concatenation that end up quoted; Zapier/n8n steps emitting an object where the API expects an array.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/9b5aaeb1b6ee7d80. Report an issue: GitHub.