Skip to content

fix(notifications): escape and truncate Telegram error messages and check API response (#5392) - #5440

Open
fliptrigga13 wants to merge 1 commit into
Dokploy:canaryfrom
fliptrigga13:fix/issue-5392-telegram-error-notifications
Open

fliptrigga13 wants to merge 1 commit into
Dokploy:canaryfrom
fliptrigga13:fix/issue-5392-telegram-error-notifications

Conversation

@fliptrigga13

@fliptrigga13 fliptrigga13 commented Sep 13, 2026

Copy link
Copy Markdown

What is this PR about?

Fixes #5392

Problem

Build-error and backup-failure notifications sent via Telegram were silently dropped in two common scenarios:

  1. Unescaped HTML entities: Telegram messages sent with parse_mode: "HTML" fail with 400 Bad Request: can't parse entities whenever the error message includes characters such as <, >, or & (very frequent in build output, shell commands, and stack traces like <--, <stdin>, etc.).
  2. Exceeding message length limits: Telegram's sendMessage caps payload text at 4,096 characters. Large build failure outputs (e.g. 20+ KB compose build logs) caused Telegram to reject the payload with 400 Bad Request: message is too long. Unlike Discord, Lark, and Teams, Telegram had no truncation limit in place.
  3. Silent failure swallowing: sendTelegramNotification in utils.ts executed fetch(url, ...) without checking response.ok, and caught errors without throwing or reporting the HTTP rejection status, making API rejections invisible.

Solution

  1. HTML Entity Escaping: Added escapeHtml in packages/server/src/utils/notifications/utils.ts to sanitize <, >, and & in error payloads sent to Telegram.
  2. Safe Length Truncation: Added formatTelegramErrorMessage which 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.
  3. Consistent Error Formatting Across Channels: Applied formatTelegramErrorMessage to all notification handlers formatting <pre>${errorMessage}</pre>:
    • packages/server/src/utils/notifications/build-error.ts
    • packages/server/src/utils/notifications/database-backup.ts
    • packages/server/src/utils/notifications/volume-backup.ts
    • packages/server/src/utils/notifications/dokploy-backup.ts
  4. API Response Validation: Checked response.ok in sendTelegramNotification and extracted response.status and error text on failures, matching sendDiscordNotification and sendSlackNotification.

Checklist

  • You created a dedicated branch based on the canary branch.
  • You have read the suggestions in the CONTRIBUTING.md file.
  • You have tested this PR in your local instance:
    • Added unit tests in apps/dokploy/__test__/notifications/telegram-error-notifications.test.ts (5/5 passing tests).

Issues related

Fixes #5392

RetriggerConfidence Score: 3/5

This PR is not safe to merge until escaped output is bounded correctly and Telegram failures no longer suppress later notification channels.

Summary

  • Escaping can expand the truncated error far beyond Telegram's payload limit.
  • Rethrowing Telegram failures can suppress all subsequently processed channels for the same notification record.
  • The added tests duplicate rather than exercise the production implementation.

Reviews (1) · Last reviewed commit: "fix(notifications): escape and truncate ..."

Comment on lines +115 to +119
const truncated =
errorMessage.length > maxLen
? `${errorMessage.substring(0, maxLen)}…`
: errorMessage;
return escapeHtml(truncated);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Escaping Breaks Length Limit

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.

Comment on lines 152 to +156
} catch (err) {
console.log(err);
console.log("error", err);
throw new Error(
`Failed to send telegram notification ${err instanceof Error ? err.message : "Unknown error"}`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +4 to +60
// Utility implementations matching packages/server/src/utils/notifications/utils.ts
const escapeHtml = (text: string): string => {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
};

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"}`,
);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests Copy Production Logic

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram build-error notifications are silently dropped: errorMessage is neither truncated nor HTML-escaped, and the API response is never checked

1 participant