{"record":{"id":"d9e495f3d353dc06","repo":"RocketChat/Rocket.Chat","slug":"invalid-reaction","errorCode":null,"errorMessage":"Invalid reaction","messagePattern":"Invalid reaction","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/app/apps/server/bridges/messages.ts","lineNumber":126,"sourceCode":"\t\t\tcase 'room':\n\t\t\t\tif (!username) {\n\t\t\t\t\tthrow new Error('Invalid username');\n\t\t\t\t}\n\n\t\t\t\tnotifications.notifyRoom(id, 'user-activity', username, isTyping ? ['user-typing'] : []);\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\tthrow new Error('Unrecognized typing scope provided');\n\t\t}\n\t}\n\n\tprivate isValidReaction(reaction: Reaction): boolean {\n\t\treturn reaction.startsWith(':') && reaction.endsWith(':');\n\t}\n\n\tprotected async addReaction(messageId: string, userId: string, reaction: Reaction): Promise<void> {\n\t\tif (!this.isValidReaction(reaction)) {\n\t\t\tthrow new Error('Invalid reaction');\n\t\t}\n\n\t\treturn executeSetReaction(userId, reaction, messageId, true);\n\t}\n\n\tprotected async removeReaction(messageId: string, userId: string, reaction: Reaction): Promise<void> {\n\t\tif (!this.isValidReaction(reaction)) {\n\t\t\tthrow new Error('Invalid reaction');\n\t\t}\n\n\t\treturn executeSetReaction(userId, reaction, messageId, false);\n\t}\n}\n","sourceCodeStart":108,"sourceCodeEnd":140,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/f9d3ec372bb580fa8d036f94cf03925a478ef768/apps/meteor/app/apps/server/bridges/messages.ts#L108-L140","documentation":"Thrown by the Apps Engine MessageBridge implementation when an App calls addReaction with a reaction string that fails the format check isValidReaction (must start and end with ':'). The bridge guards the input before delegating to executeSetReaction, which itself normalizes colons and validates the emoji against the built-in and custom emoji tables. It is a pure input-shape validation error, not a persistence or permissions error.","triggerScenarios":"An App calls the messages accessor's reaction API (e.g. modify.getUpdater().getReactionSetter() or equivalent) passing a bare emoji name like 'smile' or '+1' instead of the colon-wrapped form ':smile:'. Also triggered if the reaction string is empty or only has one colon, since startsWith(':') && endsWith(':') would be false for ':' alone when length is 1 (startsWith and endsWith both true, so ':' passes — but ':smile' or 'smile:' fail). Any reaction literal lacking both bounding colons hits this branch.","commonSituations":"Apps authored against documentation examples that omit the colons; hardcoded emoji constants copied from picker output (which often drops colons); dynamically built reaction strings where the leading/trailing colon was stripped during templating; migration from an older Apps Engine version whose API tolerated bare names.","solutions":["Wrap the reaction identifier in colons before calling addReaction: pass ':smile:' rather than 'smile'.","Centralize reaction literals in a typed constant map keyed by colon-wrapped names so callers cannot pass a bare name.","If the reaction comes from external input, run a normalize step: `reaction = reaction.startsWith(':') ? reaction : ':' + reaction; reaction = reaction.endsWith(':') ? reaction : reaction + ':';`","Verify the emoji actually exists (built-in emoji.list or a custom emoji alias) before calling, otherwise executeSetReaction will throw a separate 'Invalid emoji provided' Meteor error even after this guard passes."],"exampleFix":"// before\nawait modify.getUpdater().getReactionSetter().reactToMessage(messageId, 'thumbsup');\n\n// after\nawait modify.getUpdater().getReactionSetter().reactToMessage(messageId, ':thumbsup:');","handlingStrategy":"validation","validationCode":"function normalizeReaction(reaction: string): string {\n  if (typeof reaction !== 'string' || reaction.length === 0) {\n    throw new Error('Reaction must be a non-empty string');\n  }\n  let r = reaction;\n  if (!r.startsWith(':')) r = ':' + r;\n  if (!r.endsWith(':')) r = r + ':';\n  return r;\n}\n\n// call before addReaction\nconst safe = normalizeReaction(reaction);","typeGuard":"function isValidReaction(reaction: unknown): reaction is string {\n  return typeof reaction === 'string' && reaction.startsWith(':') && reaction.endsWith(':') && reaction.length >= 2;\n}","tryCatchPattern":null,"preventionTips":["Store all emoji constants in a single map keyed by the colon-wrapped form (':thumbsup:', ':heart:').","Run the reaction through a normalize helper at every external boundary (webhook payload, UI payload, config).","Add a unit test asserting every constant in your emoji map passes the startsWith/endsWith colon check."],"tags":["apps-engine","message-bridge","validation","reactions","input-format"],"backgroundTag":null,"analyzedSha":"f9d3ec372bb580fa8d036f94cf03925a478ef768","analyzedAt":"2026-08-12T19:07:17.372Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}