fix(delivery): drop empty chunks so Telegram doesn't reject "text must be non-empty"#38
Open
hah23255 wants to merge 1 commit intoop7418:mainfrom
Open
Conversation
…t be non-empty"
`chunkText` strips only one leading `\n` between slices. When the
splitter lands `splitIdx` at the boundary of a blank-line gap (very
common — markdown blocks separated by `\n\n` or more), the next
chunk's first character is still whitespace. After
`markdown → IR → HTML` rendering, that leading whitespace can
collapse to an empty payload — which Telegram's Bot API rejects:
400 Bad Request: text must be non-empty
The retry layer classifies HTTP 400 as `client_error` and (correctly)
does not retry, so the chunk is silently dropped and the user sees a
"[X/N part(s) failed to send — response may be incomplete]" notice
even though the rest of the response was fine.
Reproduction: any Claude reply long enough to be split into ≥ 2
chunks where the split-point is preceded by ≥ 2 newlines. Locally
this surfaces as "Chunk 2/N failed" on roughly every other long
response.
Fix is two-layer:
1. `replace(/^\n/, '')` → `replace(/^\s+/, '')` so all leading
whitespace (newlines, tabs, spaces) is consumed regardless of
how many blank lines preceded the split.
2. Defensive `chunks.filter(c => c.trim().length > 0)` at the end
so downstream code never receives a whitespace-only chunk —
even if a future chunker tuning re-introduces the class of bug.
Both `chunkText` copies (delivery-layer.ts and markdown/ir.ts) are
patched with matching commentary so the fix doesn't drift.
No test changes — existing IR/chunk tests pass with the new
behaviour because they only assert non-empty inputs.
There was a problem hiding this comment.
Pull request overview
Fixes outbound message chunking so whitespace-only chunks aren’t produced/sent (which can trigger Telegram Bot API 400 Bad Request: text must be non-empty), improving reliability for long, multi-chunk responses.
Changes:
- Update chunk split boundary handling to strip leading whitespace from subsequent slices.
- Add a defensive filter to drop all-whitespace chunks in both chunker implementations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/lib/bridge/markdown/ir.ts |
Adjusts IR text chunking to avoid leading-whitespace/empty chunks and filters whitespace-only chunks. |
src/lib/bridge/delivery-layer.ts |
Adjusts generic delivery chunking to avoid leading-whitespace/empty chunks and filters whitespace-only chunks before sending. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+55
to
+61
| // Strip ALL leading whitespace from the next slice — not only one '\n'. | ||
| // The previous `replace(/^\n/, '')` only removed a single newline, so when | ||
| // `splitIdx` landed before a run of blank lines the next chunk would start | ||
| // with leading whitespace. After markdown → IR → HTML rendering that | ||
| // whitespace can collapse to an empty payload, which Telegram rejects with | ||
| // `Bad Request: text must be non-empty`. | ||
| remaining = remaining.slice(splitIdx).replace(/^\s+/, ''); |
Comment on lines
+64
to
+67
| // Drop any chunks that ended up containing only whitespace (defensive — keeps | ||
| // downstream code from sending empty payloads regardless of how the splitter | ||
| // is tuned). | ||
| return chunks.filter((c) => c.trim().length > 0); |
Comment on lines
54
to
+67
| chunks.push(remaining.slice(0, splitIdx)); | ||
| remaining = remaining.slice(splitIdx).replace(/^\n/, ''); | ||
| // Strip ALL leading whitespace from the next slice — not only one '\n'. | ||
| // The previous `replace(/^\n/, '')` only removed a single newline, so when | ||
| // `splitIdx` landed before a run of blank lines the next chunk would start | ||
| // with leading whitespace. After markdown → IR → HTML rendering that | ||
| // whitespace can collapse to an empty payload, which Telegram rejects with | ||
| // `Bad Request: text must be non-empty`. | ||
| remaining = remaining.slice(splitIdx).replace(/^\s+/, ''); | ||
| } | ||
|
|
||
| return chunks; | ||
| // Drop any chunks that ended up containing only whitespace (defensive — keeps | ||
| // downstream code from sending empty payloads regardless of how the splitter | ||
| // is tuned). | ||
| return chunks.filter((c) => c.trim().length > 0); |
Comment on lines
+750
to
+753
| // Strip ALL leading whitespace from the next slice — not only one '\n' — | ||
| // so chunks downstream don't render to empty payloads at platform send. | ||
| // See accompanying note in lib/bridge/delivery-layer.ts. | ||
| remaining = remaining.slice(splitIdx).replace(/^\s+/, ''); |
Comment on lines
+755
to
+756
| // Drop any all-whitespace chunks defensively. | ||
| return chunks.filter((c) => c.trim().length > 0); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
chunkTextstrips only one leading\nbetween slices. When the splitter landssplitIdxat the boundary of a blank-line gap (very common — markdown blocks separated by\n\nor more), the next chunk's first character is still whitespace. Aftermarkdown → IR → HTMLrendering, that leading whitespace can collapse to an empty payload — which Telegram's Bot API rejects:The retry layer classifies HTTP 400 as
client_errorand (correctly) does not retry, so the chunk is silently dropped and the user sees a[X/N part(s) failed to send — response may be incomplete]notice even though the rest of the response was fine.Reproduction
Any Claude reply long enough to be split into ≥ 2 chunks where the split-point is preceded by ≥ 2 newlines. In real-world traffic this surfaces as a deterministic "Chunk 2/N failed" in the bridge log, on roughly every other long response.
Sample log lines from a production install over ~6 weeks (chat-id redacted):
11 of 14 failures are at chunk index #2 — which is exactly where the leading-whitespace artefact lands when the first chunk consumed everything up to the first newline of a multi-blank-line gap.
Fix
Two-layer fix:
replace(/^\n/, '')→replace(/^\s+/, '')so all leading whitespace (newlines, tabs, spaces) is consumed regardless of how many blank lines preceded the split.chunks.filter(c => c.trim().length > 0)at the end so downstream code never receives a whitespace-only chunk — even if a future chunker tuning re-introduces the class of bug.Both
chunkTextcopies (src/lib/bridge/delivery-layer.tsandsrc/lib/bridge/markdown/ir.ts) are patched with matching commentary so the fix doesn't drift.Risk
Very low. The change only affects the input boundary handling between chunks; non-empty content payloads are unchanged. No existing tests assert whitespace-only outputs, so behaviour is strictly an improvement.
Test
Existing unit tests pass unchanged. I did not add a new test in this PR to keep it minimal — happy to add one focused on the multi-blank-line split case if the maintainers prefer.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com