Telegram build-error notifications are silently dropped: errorMessage is neither truncated nor HTML-escaped, and the API response is never checked
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 37.4k
- Forks
- 3k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 73
Description
To Reproduce
- Add a Telegram notification (Settings → Notifications) and enable the App Build Error trigger. "Test Connection" confirms the bot token and chat id are fine.
- Create a Docker Compose service pointing at any repository whose build fails — e.g. a
Dockerfilewith a step that exits non-zero, or a Node project whosepackage-lock.jsonis out of sync withpackage.jsonsoRUN npm cifails withEUSAGE. - Deploy it. The build fails, the deployment row is marked
errorand the failure shows in the UI as expected. - No Telegram message arrives, and nothing is logged, so from the outside the notification simply never happens.
- Note that the App Deploy (success) notification for the same service does arrive on the same bot and chat — the channel itself is healthy, only the failure alert is missing. The Discord branch of the same code path truncates the error to 800 characters, so a Discord notification on the same trigger is not affected.
Optional direct proof of the underlying API rejection, using the same payload shape sendTelegramNotification builds:
TOKEN=<bot token> # same values Dokploy stores for the notification
CHAT=<chat id>
# 1) long error — a compose build error is ~22 KB in my instance
LONG=$(printf 'x%.0s' $(seq 1 22000))
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"chat_id\":\"$CHAT\",\"parse_mode\":\"HTML\",\"text\":\"<b>Build Failed</b>\\n<pre>$LONG</pre>\"}" \
"https://api.telegram.org/bot$TOKEN/sendMessage"
# {"ok":false,"error_code":400,"description":"Bad Request: message is too long"}
# 2) short error that contains "<" (unescaped by the HTML template)
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"chat_id\":\"$CHAT\",\"parse_mode\":\"HTML\",\"text\":\"<b>Build Failed</b>\\n<pre>process \\\"/bin/sh -c npm ci\\\" did not complete: exit 1 <-- see log</pre>\"}" \
"https://api.telegram.org/bot$TOKEN/sendMessage"
# {"ok":false,"error_code":400,"description":"Bad Request: can't parse entities: Unsupported start tag \"--\" at byte offset 100"}
Current vs. Expected behavior
Current: build-error notifications never reach Telegram, even though success notifications on the same connection do. Two independent causes, both in the Telegram path.
-
No truncation.
packages/server/src/utils/notifications/build-error.tsinterpolates the raw error into the Telegram template (line 204 oncanary):`<b>⚠️ Build Failed</b>\n\n…\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`For a Docker Compose deployment,
error.messageis theExecErrorof the generated shell command, so it embeds the whole script (base64 compose file, thedocker compose … up -d --buildinvocation, the redirect to the log file) plus the build output. In my instance that message is 22,312 bytes, and Telegram'ssendMessagecapstextat 4096 characters →400 Bad Request: message is too long.The other channels in the same file already guard against this — Discord (line 111), Lark (line 292) and Teams (line 412) all do
const limitCharacter = 800; errorMessage.substring(0, limitCharacter). Only the Telegram branch passes the error through untouched. -
No HTML escaping. The template is sent with
parse_mode: "HTML", so an error containing<— common in build output (<--,<stdin>, generics) — yields400 Bad Request: can't parse entities, regardless of length.
Both rejections are invisible, because sendTelegramNotification in packages/server/src/utils/notifications/utils.ts never inspects the response:
try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
await fetch(url, { method: "POST", /* … */ }); // no `if (!response.ok)` check
} catch (err) {
console.log(err);
}
sendSlackNotification, immediately below, does if (!response.ok) throw new Error(...). Since fetch does not reject on HTTP 4xx, the Telegram helper cannot even in principle report a rejected message: no error, no log line.
Expected: the Telegram text is truncated to fit the 4096-character limit and HTML-escaped (or sent without parse_mode), so build-error alerts arrive like they do on Discord — and a non-2xx response from the Telegram API is at least logged.
The same <pre>${errorMessage}</pre> pattern appears in database-backup.ts, volume-backup.ts and dokploy-backup.ts, so long or HTML-bearing errors in those flows are dropped the same way. Notifications that carry no error text (Build Success, Dokploy Restart) arrive normally, which is what makes this easy to miss: the channel looks healthy and only the failures go missing.
Impact: in my setup four consecutive failed deploys of the same service went unnoticed between 2026-08-27 and 2026-08-31. A failed build leaves the previous container running, so the service keeps answering 200 while serving stale code, and the alert that should have caught it was silently rejected by the API.
Provide environment information
Operating System:
OS: Ubuntu 22.04.1 LTS
Arch: aarch64 (arm64)
Docker: 27.5.1
Dokploy version: 0.29.6
VPS Provider: Oracle Cloud
What applications/services are you trying to deploy?
Docker Compose service (Node/Fastify app built from a Dockerfile), GitHub auto-deploy on push
Notification channels configured: Telegram (App Deploy + App Build Error) and Discord (same triggers)
Which area(s) are affected? (Select all that apply)
Docker Compose
Are you deploying the applications where Dokploy is installed or on a remote server?
Same server where Dokploy is installed
Additional context
A minimal fix mirrors what the other channels already do, in every file that builds a Telegram message from errorMessage:
const escapeHtml = (s: string) =>
s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
const limitCharacter = 3000;
const truncatedErrorMessage = escapeHtml(
errorMessage.length > limitCharacter
? `${errorMessage.substring(0, limitCharacter)}…`
: errorMessage,
);
plus a response check in sendTelegramNotification, so a rejected message is no longer silent:
const response = await fetch(url, { /* … */ });
if (!response.ok) {
throw new Error(
`Failed to send telegram notification: ${response.status} ${await response.text()}`,
);
}
I searched the existing issues first: #3589, #1305, #681 and #2895 cover other Telegram topics (server-restart wording, topics/thread ids, webhook channels) and none describe the silent drop of long or unescaped error messages.
Will you send a PR to fix it?
No
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/server/src/utils/notifications/build-error.ts and packages/server/src/utils/notifications/utils.ts, comparing the Telegram path with the existing Discord, Lark, Teams, and Slack handling. Review the Telegram message construction in database-backup.ts, volume-backup.ts, and dokploy-backup.ts as well. Done means error text is safely bounded and HTML-escaped, and rejected Telegram API responses are logged rather than silently ignored.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, docker-compose, typescript
- Domain
- backend, devops
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100