Dokploy / Dokploy/dokploy

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

Open
#5,392 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
37.4k
Forks
3k
Avg merge
1d 3h
Merged PRs (30d)
73

Description

To Reproduce
  1. Add a Telegram notification (Settings → Notifications) and enable the App Build Error trigger. "Test Connection" confirms the bot token and chat id are fine.
  2. Create a Docker Compose service pointing at any repository whose build fails — e.g. a Dockerfile with a step that exits non-zero, or a Node project whose package-lock.json is out of sync with package.json so RUN npm ci fails with EUSAGE.
  3. Deploy it. The build fails, the deployment row is marked error and the failure shows in the UI as expected.
  4. No Telegram message arrives, and nothing is logged, so from the outside the notification simply never happens.
  5. 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.

  1. No truncation. packages/server/src/utils/notifications/build-error.ts interpolates the raw error into the Telegram template (line 204 on canary):

    `<b>⚠️ Build Failed</b>\n\n…\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
    

    For a Docker Compose deployment, error.message is the ExecError of the generated shell command, so it embeds the whole script (base64 compose file, the docker compose … up -d --build invocation, the redirect to the log file) plus the build output. In my instance that message is 22,312 bytes, and Telegram's sendMessage caps text at 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.

  2. No HTML escaping. The template is sent with parse_mode: "HTML", so an error containing < — common in build output (<--, <stdin>, generics) — yields 400 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.