fix(notifications): escape and truncate Telegram error messages and check API response (#5392) - #5440
Conversation
…heck API response (Dokploy#5392)
| const truncated = | ||
| errorMessage.length > maxLen | ||
| ? `${errorMessage.substring(0, maxLen)}…` | ||
| : errorMessage; | ||
| return escapeHtml(truncated); |
There was a problem hiding this comment.
Truncating before HTML escaping does not enforce Telegram's length limit. For example, 3,000 ampersands expand to 15,000 characters after escaping, so Telegram still rejects the notification as too long. Bound the escaped output without cutting an HTML entity, or calculate the output within the final payload budget.
| } catch (err) { | ||
| console.log(err); | ||
| console.log("error", err); | ||
| throw new Error( | ||
| `Failed to send telegram notification ${err instanceof Error ? err.message : "Unknown error"}`, | ||
| ); |
There was a problem hiding this comment.
Telegram Failure Skips Channels
This now rethrows Telegram API and network failures, but each notification handler wraps the entire channel sequence in one try/catch. Because Telegram runs before Slack, Mattermost, custom, Lark, Pushover, and Teams, a Telegram failure prevents those later configured channels from receiving the same alert.
| // Utility implementations matching packages/server/src/utils/notifications/utils.ts | ||
| const escapeHtml = (text: string): string => { | ||
| return text | ||
| .replace(/&/g, "&") | ||
| .replace(/</g, "<") | ||
| .replace(/>/g, ">"); | ||
| }; | ||
|
|
||
| const formatTelegramErrorMessage = ( | ||
| errorMessage: string, | ||
| maxLen = 3000, | ||
| ): string => { | ||
| const truncated = | ||
| errorMessage.length > maxLen | ||
| ? `${errorMessage.substring(0, maxLen)}…` | ||
| : errorMessage; | ||
| return escapeHtml(truncated); | ||
| }; | ||
|
|
||
| const sendTelegramNotification = async ( | ||
| connection: { | ||
| botToken: string; | ||
| chatId: string; | ||
| messageThreadId?: string; | ||
| }, | ||
| messageText: string, | ||
| inlineButton?: { text: string; url: string }[][], | ||
| fetchMock = fetch, | ||
| ) => { | ||
| try { | ||
| const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; | ||
| const response = await fetchMock(url, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| chat_id: connection.chatId, | ||
| message_thread_id: connection.messageThreadId, | ||
| text: messageText, | ||
| parse_mode: "HTML", | ||
| disable_web_page_preview: true, | ||
| reply_markup: { | ||
| inline_keyboard: inlineButton, | ||
| }, | ||
| }), | ||
| }); | ||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error( | ||
| `Failed to send telegram notification: ${response.status} ${errorText}`, | ||
| ); | ||
| } | ||
| } catch (err) { | ||
| throw new Error( | ||
| `Failed to send telegram notification ${err instanceof Error ? err.message : "Unknown error"}`, | ||
| ); | ||
| } | ||
| }; |
There was a problem hiding this comment.
These tests reimplement all three production functions and therefore execute no production notification code. They will keep passing if the real escaping, truncation, exports, fetch handling, or handler wiring regresses. Import the exported utilities and mock global fetch instead so CI covers the actual implementation.
What is this PR about?
Fixes #5392
Problem
Build-error and backup-failure notifications sent via Telegram were silently dropped in two common scenarios:
parse_mode: "HTML"fail with400 Bad Request: can't parse entitieswhenever the error message includes characters such as<,>, or&(very frequent in build output, shell commands, and stack traces like<--,<stdin>, etc.).sendMessagecaps payload text at 4,096 characters. Large build failure outputs (e.g. 20+ KB compose build logs) caused Telegram to reject the payload with400 Bad Request: message is too long. Unlike Discord, Lark, and Teams, Telegram had no truncation limit in place.sendTelegramNotificationinutils.tsexecutedfetch(url, ...)without checkingresponse.ok, and caught errors without throwing or reporting the HTTP rejection status, making API rejections invisible.Solution
escapeHtmlinpackages/server/src/utils/notifications/utils.tsto sanitize<,>, and&in error payloads sent to Telegram.formatTelegramErrorMessagewhich safely truncates error messages exceeding 3,000 characters and applies HTML escaping to ensure the total payload remains comfortably under Telegram's 4,096 limit.formatTelegramErrorMessageto all notification handlers formatting<pre>${errorMessage}</pre>:packages/server/src/utils/notifications/build-error.tspackages/server/src/utils/notifications/database-backup.tspackages/server/src/utils/notifications/volume-backup.tspackages/server/src/utils/notifications/dokploy-backup.tsresponse.okinsendTelegramNotificationand extractedresponse.statusand error text on failures, matchingsendDiscordNotificationandsendSlackNotification.Checklist
canarybranch.apps/dokploy/__test__/notifications/telegram-error-notifications.test.ts(5/5 passing tests).Issues related
Fixes #5392
This PR is not safe to merge until escaped output is bounded correctly and Telegram failures no longer suppress later notification channels.
Summary
Reviews (1) · Last reviewed commit: "fix(notifications): escape and truncate ..."