fix(line): add chunk-idle timeout to inbound media download#86873
fix(line): add chunk-idle timeout to inbound media download#86873masatohoshino wants to merge 1 commit into
Conversation
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 2:49 PM ET / 18:49 UTC. Summary PR surface: Source +107, Tests +108. Total +215 across 2 files. Reproducibility: yes. source-reproducible: current main awaits LINE media headers and stream persistence before Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the focused LINE-local idle timeout if maintainers accept the fixed 30-second per-chunk attachment policy; otherwise request a config or overall-deadline adjustment before merge. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible: current main awaits LINE media headers and stream persistence before Is this the best way to solve the issue? Yes, with maintainer policy acceptance. A LINE-local idle timeout is the narrowest owner-boundary mitigation I found; bypassing the LINE SDK for direct abortable fetch would be broader and would duplicate endpoint/auth behavior. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 4c841ac575d7. Label changesLabel justifications:
Evidence reviewedPR surface: Source +107, Tests +108. Total +215 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
ClawSweeper PR egg ✨ Hatched: 🥚 common Moonlit Clawlet Hatch commandComment Hatchability rules:
Rarity: 🥚 common. What is this egg doing here?
|
|
@clawsweeper re-review |
|
🦞👀 Command router queued. I will update this comment with the next step. Re-review progress:
|
|
This pull request has been automatically marked as stale due to inactivity. |
|
ClawSweeper status: review started. I am starting a fresh review of this pull request: fix(line): add chunk-idle timeout to inbound media download This is item 1/1 in the current shard. Shard 26/31. This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking. Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted. |
|
Closing due to inactivity on my side — open since May with unresolved conflicts. Withdrawing to keep the queue clean. The chunk-idle timeout gap it describes may still be worth fixing, so I may return with a rebased version. |
Summary
downloadLineMediainextensions/line/src/download.tswraps the LINEMessaging API
getMessageContentresponse in afor-awaitloop with noread deadline. When the LINE CDN returns HTTP headers but then stalls the
body stream, the function blocks indefinitely. Because
extensions/line/src/bot-handlers.ts:467-482awaits the result beforecalling
processMessage, the entire inbound dispatch hangs: no agent replyis produced and the conversation goes silent.
This PR adds a per-chunk idle timeout (
LINE_DOWNLOAD_IDLE_TIMEOUT_MS = 30_000) to cap both the headers-level fetch and each stream chunk read.The ceiling matches the existing Telegram pattern
(
extensions/telegram/src/bot/delivery.resolve-media.ts:161).The timeout is idle (per-chunk), not overall, so legitimate slow-but-
progressing transfers continue.
Affected channel: LINE inbound media only.
Unaffected: Telegram, WhatsApp, MSTeams, Discord, Slack, Matrix,
all other channels. Pure-text LINE messages. LINE outbound path.
Behavior change
LineMediaDownloadTimeoutError(30_000)after 30 s; existing catch atbot-handlers.ts:474-481dispatches without mediaImplementation notes
withChunkIdleTimeout<T>()wraps anyAsyncIterable<T>so eachnext()call races against a
setTimeout. On timeout,iterator.return()iscalled, which triggers
Readable.destroy()for Node streams from@line/bot-sdk.@line/bot-sdkHTTPFetchClient.getdoes not acceptAbortSignal, sothe underlying
fetchcannot be cancelled. On timeout, the functionreturns promptly but the background fetch may continue until TCP-level
timeout. This is an acceptable trade-off; the caller proceeds without
blocking.
next()andgetMessageContent()promises are explicitly silenced(
.then(noop, noop)) so a later rejection from the destroyed upstreamcannot escape as an
unhandledRejectionon Node 22+.bot-handlers.ts:474-481already handlesarbitrary errors via
String(err). The new error message includes"stalled", which routes to theruntime.error?.(danger(...))branch(not the silent
"exceeds limit"branch), so stalls are logged anddispatch continues without media.
chunkTimeoutMsis an optional parameter so tests can use short valueswithout touching the exported constant; no LINE config schema entry is
added (out of scope for this PR; maintainer can request one if desired).
(
(1 headers race) + (N chunk races)ofchunkTimeoutMseach), not asingle 30 s shared deadline. With the default 30 s ceiling this is well
inside LINE's 30 s webhook-ACK budget for the realistic single-stall
case; deeply stalled CDNs with partial-progress flapping could exceed it
but a follow-up overall-deadline option can be added if maintainers
prefer.
Files changed
extensions/line/src/download.ts(+113 / −6): addsLINE_DOWNLOAD_IDLE_TIMEOUT_MS,LineMediaDownloadTimeoutError,withChunkIdleTimeout(), wiresoptions?.chunkTimeoutMsthrough boththe headers-level race and the stream-consumption phase, and silences
the orphaned upstream promises on timeout so they cannot become
unhandledRejection.extensions/line/src/download.test.ts(+109 / −1): adds achunk-idle timeoutdescribe block with 6 focused regression tests.Non-scope (deliberately separate PRs)
downloadInboundMedia(extensions/whatsapp/src/inbound/media.ts) — same root pattern (no abort/timeout), separate PR.resolveMSTeamsInboundMedia(extensions/msteams/src/monitor-handler/inbound-media.ts) — same root pattern, separate PR.chunkTimeoutMs.processMessagewhile media hydrates in background) — would require restructuringbot-handlers.tsand is broader than the LINE narrow ceiling.Real behavior proof
Current HEAD SHA:
c10307cd7841cd3fe8df0ae92694f8b534153ba1. Proof captured against this exact HEAD; this body update is parser-label normalization only — no code, test, or config diff change.Behavior addressed:
LINE inbound media download blocks indefinitely when the LINE CDN stalls the response body stream, preventing
processMessagefrom being called and leaving the conversation silent. The contributor ask is to prove that, after this patch, a stalled LINE media boundary no longer blocks inbound dispatch —downloadLineMediarejects within the configured ceiling, the existing catch path inbot-handlers.ts:467-499logs the failure,buildLineMessageContextis still called, andprocessMessageis reached.Real environment tested:
A local Node 22 process running the actual extension source via
pnpm tsx, driving the realhandleLineWebhookEventsentry point with a controlled stall installed at the@line/bot-sdkMessagingApiBlobClient.prototype.getMessageContentboundary. The production source — unmodifiedbot-handlers.tsdispatch path, unmodifieddownloadLineMedia, unmodifiedbuildLineMessageContext, realopenclaw/plugin-sdk/media-store, real ingress policy helpers — is exercised end-to-end.processMessageis the only spy. This is not a live LINE channel test; live LINE-CDN-side TCP behavior under partial-progress flapping is therefore not covered (see What was not tested).Exact steps or command run after this patch:
The harness file
extensions/line/scratch/handler-stall-proof.mtsis not committed; it patchesMessagingApiBlobClient.prototype.getMessageContentfor the chosen scenario, then awaits the realhandleLineWebhookEventswith a minimalLineHandlerContextwhoseprocessMessageis a JSON-emitting spy. Production defaultLINE_DOWNLOAD_IDLE_TIMEOUT_MS = 30_000is used for all five scenarios.Evidence after fix:
Redacted terminal capture from each
pnpm tsxrun, summarized into one row per scenario. Production defaultchunkTimeoutMs = 30_000was used everywhere.runtime.errorcaptures the exact log string the production catch path would emit.@line/bot-sdkboundarydownloadLineMediaoutcomeruntime.errorfiredprocessMessagereachedmedia_countunhandledRejectiongetMessageContentresolves to async iterable whosenext()never settlesLineMediaDownloadTimeoutError(30000)getMessageContentreturns a Promise that never resolvesLineMediaDownloadTimeoutError(30000)next()never settlesLineMediaDownloadTimeoutError(30000)(per-chunk budget reset works)Sample raw JSON output from the
bodyscenario (redacted timestamps):{ "scenario": "body", "chunk_timeout_ms_default": 30000, "download_outcome": "timed-out", "process_message_called": true, "handler_settled": true, "handler_threw": false, "runtime_error_calls": [ "line: failed to download media: LineMediaDownloadTimeoutError: LINE media download stalled: no data received for 30000ms" ], "unhandled_rejection_seen": false, "process_message_media_count": 0, "handler_elapsed_ms": 30029, "download_elapsed_ms": 30029, "download_error_name": "LineMediaDownloadTimeoutError" }Observed result after fix:
With the production default 30 s ceiling, the three stall scenarios (
body,headers,partial-stall) each terminated in exactly oneLineMediaDownloadTimeoutErrorat ~30 029 ms wall time, routed through the existingbot-handlers.ts:474-481catch toruntime.error?.(danger(...))(the"stalled"branch, not the silent"exceeds limit"branch).buildLineMessageContextwas still invoked andprocessMessagewas reached with zero media attachments. The two happy-path scenarios (fast,slow-progress) completed promptly and delivered one media attachment toprocessMessage. NounhandledRejectionsurfaced in any scenario after a 1 500 ms post-handler settle window — the orphan-rejection suppression inwithChunkIdleTimeoutand at the headers race both hold under real Node 22 event-loop scheduling.What was not tested:
downloadLineMedialatency (no live traffic instrumented).fetchinside@line/bot-sdkHTTPFetchClient.getis acknowledged in Implementation notes but is not verified to settle within any specific TCP-level bound; that is a@line/bot-sdkSDK limitation, not introduced by this PR. The handler-level runtime proof above confirms the orphan does not surface as anunhandledRejectionin the 1 500 ms window afterhandleMessageEventreturns.Unit coverage
extensions/line/src/download.test.tsadds 6 focused regression tests under thechunk-idle timeoutdescribe block. These are supplemental to the handler-level runtime proof above and validate thewithChunkIdleTimeoutwrapper in isolation against an@line/bot-sdkstub.chunkTimeoutMsLineMediaDownloadTimeoutErrorLineMediaDownloadTimeoutErrorLINE_DOWNLOAD_IDLE_TIMEOUT_MS === 30_000iterator.return()called exactly onceLineMediaDownloadTimeoutErrorSuite results on the current HEAD:
extensions/line/src/download.test.ts12 passed;extensions/line270 passed;pnpm check:changedgreen;pnpm format:check extensions/line/src/download.ts extensions/line/src/download.test.tsclean.