vercel / vercel/chat

Unify length handling across adapters: throw `MessageTooLongError`, remove silent data loss

Open
#408 0 comments 2 reactions 0 assignees View on GitHub

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
WhatsApp 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, LongMessageStrategy config type, messageLimits metadata.
  • 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 splitMessage becomes 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 unionpost(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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.