RocketChat/Rocket.Chat · error · Meteor.Error
emoji-is-not-image
emoji-is-not-image
Error message
Emoji file provided cannot be uploaded since it's not an image
What it means
Thrown by POST emoji-custom.create when the uploaded file's content is not recognized as an image. Rocket.Chat runs Media.isImage(fileBuffer), which sniffs the buffer's magic bytes (file-type style detection) rather than trusting the declared mimetype or file extension, so renaming a non-image to .png will not pass. This is a content-type safety check: custom emojis are served to all clients as images.
Solutions
- Upload a real raster image (PNG, JPG/JPEG, GIF) as the 'emoji' multipart field - convert SVG to PNG first
- Verify the file locally before uploading by sniffing its content, not its name (e.g. `file emoji.png` or the file-type npm package)
- If the file should be valid, re-export it from the source tool - a truncated download or 0-byte file also fails the sniff
- Check you are reading the file from disk correctly in the client (fs.readFileSync vs accidentally passing a path string or JSON body)
Example fix
// before
const form = new FormData();
form.append('emoji', fs.createReadStream('logo.svg'), 'logo.svg'); // SVG rejected by content sniffing
// after
const form = new FormData();
form.append('emoji', fs.createReadStream('logo.png'), 'logo.png'); Defensive patterns
Strategy: validation
Validate before calling
import { fromBuffer } from 'file-type';
async function assertUploadableImage(buffer) {
const type = await fromBuffer(buffer);
if (!type || !/^image\/(png|jpeg|gif)$/.test(type.mime)) {
throw new Error(`Not an uploadable image (detected: ${type?.mime ?? 'unknown'})`);
}
} Type guard
const isImageBuffer = async (buf: Buffer): Promise<boolean> => {
const t = await fromBuffer(buf);
return !!t && ['image/png', 'image/jpeg', 'image/gif'].includes(t.mime);
}; Prevention
- Sniff magic bytes locally with the file-type package before uploading - never trust extensions
- Convert design assets (SVG) to PNG in your build step, not at upload time
- In bulk importers, pre-filter the directory and log skipped non-image files instead of failing the whole run
When it happens
Trigger: POST /api/v1/emoji-custom.create with multipart field 'emoji' containing a PDF, ZIP, text file, or SVG (SVG is typically not detected as a binary image type by magic-byte sniffing and gets rejected). Also a truncated/corrupted image whose header bytes are damaged, or an empty buffer with a fake image mimetype.
Common situations: Design teams exporting emoji assets as SVG and uploading directly; automated import scripts feeding whatever file sits in a directory; files that lost bytes in transit (wrong multipart sizeLimit handling); users renaming files to bypass extension checks.
Related errors
- Emoji not found.
- error-avatar-invalid-content-type
- error-message-size-exceeded
- error-no-file-uploaded
- error-roomId-param-invalid
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/d0c88ef6ea944e40.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/server/api/v1/emoji-custom.ts:202
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const emoji = await getUploadFormData(
{
request: this.request,
},
{
field: 'emoji',
sizeLimit: settings.get('FileUpload_MaxFileSize'),
},
);
const { fields, fileBuffer, mimetype } = emoji;
const isUploadable = await Media.isImage(fileBuffer);
if (!isUploadable) {
throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image");
}
const [, extension] = mimetype.split('/');
fields.extension = extension;
const emojiData = await insertOrUpdateEmoji(this.userId, {
...fields,
newFile: true,
aliases: fields.aliases || '',
name: fields.name,
extension: fields.extension,
});
await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData);
return API.v1.success();
},
)View on GitHub (pinned to b2c16d5842)