Unify length handling across adapters: throw `MessageTooLongError`, remove silent data loss
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 314
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 63
Description
Problem
Adapters handle over-limit messages with three incompatible strategies, all lossy or opaque:
| Adapter | Limit | Behavior |
|---|---|---|
| Slack | 40,000 / 3,000 section | No check — platform returns untyped 400 |
| Discord | 2,000 | Silent truncate + ... (same bug class fixed in #PR) |
| Google Chat | 4,000 | No check — platform 400 |
| Teams | ~28,000 | No check — platform 400 |
| 4,096 | Auto-split into N posts, returns only last chunk's ID | |
| Telegram | 4,096 / 1,024 caption | Silent truncate with safe-boundary trim (just fixed) |
Each strategy is surprising in its own way. Silent truncation loses caller content. Silent splitting loses message identity (reactions / edits target only the last chunk). Platform 400s surface as generic errors the caller can't branch on.
Discord specifically has the same unclosed-entity and orphan-backslash bugs the recent Telegram fix addressed — just undetected because few messages hit 2,000 chars.
First-principles argument
post(message): Promise<Sent> claims to be total. When input exceeds the platform limit, the returned "Sent" either lost content, silently became N messages, or the promise rejected with an untyped error. The signature is lying in three different ways across adapters.
The honest contract: platform length limits are part of the adapter's interface and should be surfaced, not hidden.
Proposal
Default: throw a typed error. Consistent with how the SDK already handles ValidationError, AuthenticationError, RateLimitError. Exceptions carry stack traces and structured context; callers handle overflow at the scope that makes sense (one try/catch around a handler) without every call site branching on status.
class MessageTooLongError extends ValidationError {
readonly limit: number;
readonly actualLength: number;
readonly platform: string;
}
Expose limits as adapter metadata:
interface Adapter {
readonly messageLimits: { text: number; caption?: number };
}
Opt-in strategies at adapter construction for callers who want non-default policy:
type LongMessageStrategy = "throw" | "truncate" | "split" | "file";
Default is "throw". Existing behaviors move to opt-in:
"truncate"— current Telegram/Discord silent-trim behavior"split"— current WhatsApp multi-message behavior"file"— above-limit body attached as.txt, short caption inline
Scope by adapter
- Shared: add
MessageTooLongError,LongMessageStrategyconfig type,messageLimitsmetadata. - Telegram: current truncator becomes the
"truncate"path (safe-boundary trim already in place). Default flips to"throw". - Discord: promote current truncate to opt-in, port the safe-boundary-trim logic (Discord shares the unclosed-entity and orphan-
\bugs). - WhatsApp: current
splitMessagebecomes the"split"path. Default flips to"throw"— docs explain why auto-split breaks reactions/edits/replies. - Slack / GChat / Teams: add proactive length check before API call. Currently relies on platform 400. After: typed error with same timing, actionable by caller.
Alternatives considered
Discriminated return union — post(msg): Promise<{status: "sent"} | {status: "too_long"}>. Rejected: inconsistent with every other constraint in the SDK (all throw), and forces every call site to branch on status even when the message is known-safe, which is the vast majority of calls. Try/catch at a sensible scope is cheaper than type noise everywhere.
Keep current per-adapter behavior, document it — Rejected: the behaviors are not discoverable from the Adapter interface, and "silent" is the common failure mode. Documentation doesn't prevent data loss.
Global default of "truncate" — Rejected: lossy by default is what got us here. Callers should opt into any behavior that drops bytes.
Non-goals
- Changing the AST layer or format-converters.
- Changing how card / attachment length limits are enforced (separate concern).
Migration
Breaking change for callers relying on silent truncation / splitting. Gated behind a minor version bump of each adapter; release notes call out the default flip and the opt-in path to restore prior behavior. Could also ship "throw" as opt-in first for one minor release, warn when the overflow path is hit, then flip in the next minor.
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 by locating the Adapter implementations for Telegram, Discord, WhatsApp, Slack, Google Chat, and Teams, then read the existing ValidationError handling and truncation or splitting paths. Compare each adapter's current overflow behavior with the proposed shared error, metadata, and strategy contract. Done means all listed adapters consistently default to typed overflow errors while prior lossy behaviors are explicit opt-ins and the migration is documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100