fix(line): precheck outbound LINE media size#84229
Conversation
|
Codex review: needs maintainer review before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. from source inspection and the LINE platform contract. Current main validates outbound LINE media URLs but does not precheck Content-Length before push/reply calls, so over-cap media can still be handed to LINE and fail outside OpenClaw's returned result. PR rating Rank-up moves:
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. Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land the LINE-local guarded precheck if maintainers accept the compatibility, egress, and latency tradeoffs, and track generic quick-reply inline coverage separately if full outbound coverage is required. Do we have a high-confidence way to reproduce the issue? Yes, from source inspection and the LINE platform contract. Current main validates outbound LINE media URLs but does not precheck Content-Length before push/reply calls, so over-cap media can still be handed to LINE and fail outside OpenClaw's returned result. Is this the best way to solve the issue? Mostly yes. A plugin-local guarded HEAD precheck uses the existing SSRF guard and keeps the fix near the LINE send paths; the only maintainer choice is accepting the local fail-closed and latency tradeoffs or asking for adapter probe deduping first. Label changes:
Label justifications:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 6ccca4ae9529. |
|
Updated current-head real behavior proof for 3642d23, including previewImageUrl 1 MiB cap rejection and refreshed under-cap delivery / over-cap rejection evidence. |
3642d23 to
37c6fae
Compare
|
ClawSweeper PR egg ✨ Hatched: 🥚 common Moonlit Review Wisp Hatch commandComment Hatchability rules:
Rarity: 🥚 common. What is this egg doing here?
|
resolveLineOutboundMedia and pushImageMessage now apply the stricter 1 MiB LINE preview cap when no explicit previewImageUrl is supplied. Downstream buildLineMediaMessageObject (outbound.ts) and createImageMessage (send.ts) default previewImageUrl to the originalContentUrl in the image branch, so an image between 1 MiB and 10 MiB previously passed the local 10 MiB image cap and was rejected later by LINE's 1 MiB preview cap asynchronously. Regression tests added: - outbound-media.test.ts: resolveLineOutboundMedia image kind with no explicit preview and a 5 MiB mediaUrl rejects with the preview cap. - send.test.ts: pushImageMessage with a 3 MiB originalContentUrl and no explicit preview rejects with the preview cap; one HEAD probe. Explicit same-URL preview handling is unchanged; the merged branch now also catches the implicit case. Video and audio paths are unaffected.
|
Closing this for now — it's a larger change that has drifted (conflicting/stale base) and no longer fits the small, correctness-focused PRs I'm prioritizing. The proof still stands if it's wanted later and I'm happy to refresh and reopen. Thanks! |
Summary
Outbound LINE media (image / video / audio / preview) currently returns
success from
pushMessage/replyMessageeven when the URL body islarger than the LINE platform cap; LINE rejects the URL asynchronously
after our call returns, so the caller sees no failure but the recipient
never receives the message.
This PR adds a local size precheck on every outbound LINE media call
site. Hosts that don't expose
Content-Lengthcontinue to workunchanged (soft-fail, pass through).
Behavior change
LINE_OUTBOUND_MEDIA_MAX_BYTESper role (caps documented at theLINE Messaging API reference,
verified 2026-05-20) and
precheckLineOutboundMediaSizeinextensions/line/src/outbound-media.ts. The helper issues a HEAD viathe existing
fetchWithSsrFGuard(5 s,requireHttps,mode: "strict",LINE_OUTBOUND_MEDIA_SSRF_POLICY—allowPrivateNetwork: false).200or206withContent-Length > cap.All other statuses (
2xxwithout a usable body length,3xx,4xx,5xx, network error) take the soft-fail branch and pass through;the size contract is only enforceable when the host advertises a
concrete
Content-Lengthalongside a200/206response.validateLineMediaUrlsite:resolveLineOutboundMedia, the video / audio / image branches ofsendMessageLine, andpushImageMessage.previewImageUrlisalways checked against the stricter 1 MiB preview cap.
resolveLineOutboundMediaandpushImageMessagedefaultpreviewImageUrltooriginalContentUrl. An image between 1 MiB and10 MiB therefore passed the local image cap but was rejected later by
LINE's preview cap. After this PR the implicit case binds against
the preview cap first.
previewImageUrl === mediaUrl(explicit or via theimplicit fallback), the helper issues a single HEAD probe and
evaluates it against the stricter preview cap.
Files
LINE-channel-local; no shared SDK, core, or cross-channel surface is
touched.
Production change is approximately +139 LOC; the remaining
+471 LOC is unit tests covering cap math (per role, inclusive
boundary), soft-fail branches (probe error, non-2xx, missing /
malformed / negative
Content-Length), equal-URL dedupe, and theimplicit-preview fallback in both
resolveLineOutboundMediaandpushImageMessage. The overall diff size therefore reflects testcoverage rather than production surface area, and the production
change is confined to
extensions/line/src/**.Non-Scope
src/media/**orsrc/plugin-sdk/**. Independent offix(media): add shared media MIME validation helpers #76566 (MIME helpers — no file overlap).
extensions/line/src/outbound.ts, untouched) chainsresolveLineOutboundMedia→sendMessageLine. Both layers precheck,so the kind-aware adapter path issues two HEAD probes per media URL.
This matches the existing topology used by
validateLineMediaUrl(same call sites, same duplication), and collapsing it requires
touching
outbound.tswhich is out of scope for this size-cap PR.branch in
extensions/line/src/outbound.ts(the path that pushes animage object directly when no LINE-specific media options are
present) is intentionally not covered by this precheck. Scope
here is restricted to outbound image / video / audio / preview URL
precheck reachable through
sendMessageLine,pushImageMessage,and
resolveLineOutboundMedia. A follow-up PR can extend theprecheck to the quick-reply inline branch if maintainers want full
outbound coverage.
Verification
vitest extensions/line/src/outbound-media.test.tsvitest extensions/line/src/send.test.tsnode scripts/test-projects.mjs --changed upstream/mainReal behavior proof
Behavior addressed: outbound LINE media (image / video / audio /
preview) URLs whose body exceeds the LINE platform cap previously
returned success from
pushMessage/replyMessagewhile LINErejected the URL asynchronously, so the recipient never received the
message. The new precheck makes this fail locally with a descriptive
error before the LINE API call is issued.
Real environment tested: live LINE Messaging API channel against a
1:1 chat target, using
extensions/line/src/runtime-api.tsexportedhelpers from the rebuilt worktree (
dist/extensions/line/runtime-api.js).Media fixtures were hosted on a short-lived Cloudflare quick-tunnel,
which was torn down after the proof window. The fixture host only
served three deterministic byte-length files (small valid, mid
between 1 MiB and 10 MiB, and over 10 MiB).
Exact steps or command run after this patch:
pnpm build).python3 -m http.server 18890 --bind 127.0.0.1.cloudflared tunnel --url http://127.0.0.1:18890.runtime/secrets/.envto populateLINE_CHANNEL_ACCESS_TOKEN/LINE_CHANNEL_SECRET.dist/extensions/line/runtime-api.jsand callssendMessageLine/pushImageMessagewith each of cases A / B / C / D below.Evidence after fix: cases B, C, D fail locally with the redacted
runtime log output captured below (terminal output from
node driver.mjsagainst the rebuilt worktree'sdist/extensions/line/runtime-api.js);client.pushMessageis notinvoked in those cases. Case A succeeds end-to-end with the LINE app
receiving the message. The fixture-host access log tail confirms
exactly 1 HEAD per distinct URL, and 0 HEADs on the rejected payload
for soft-fail.
Observed result after fix:
sendMessageLineimage,previewImageUrl === mediaUrl, file = 3 116 B → LINE app received the message. Origin log: 1 HEAD (equal-URL dedupe) followed by LINE-side GET.sendMessageLineimage,originalContentUrl14 545 753 B (> 10 MiB) + valid small preview → local image-cap error;client.pushMessagenot called. Origin log: 1 HEAD on the over-cap original, no preview HEAD, no LINE-side GET.sendMessageLineimage, valid smalloriginalContentUrl+previewImageUrl1 489 176 B (between 1 MiB and 10 MiB) → local preview-cap error;client.pushMessagenot called.sendMessageLineimage,previewImageUrl === mediaUrl, shared file 1 489 176 B → local preview-cap error; exactly 1 HEAD for the shared URL (dedupe preserved, stricter cap applied).What was not tested:
outbound-media.test.tswith a syntheticContent-Length: 209715201HEAD response).extensions/line/src/outbound.tsadapter path; the precheck behavior is identical to the directsendMessageLinepath proven above.outbound-media.test.tsandsend.test.ts.pushImageMessage(previewImageUrl === originalContentUrlshared-file case) live capture is not included in the redacted fenced block above; its preview-cap rejection is covered by unit testsend.test.tspushImageMessage preview-cap rejection, which asserts the same shape as the livesendMessageLineCase D above.Appendix: caps, test additions
Cap values (verified at the LINE Messaging API reference linked above
on 2026-05-20):
originalContentUrloriginalContentUrloriginalContentUrlpreviewImageUrl(image / video)Test additions:
outbound-media.test.ts(explicit-preview rejection inresolveLineOutboundMedia, equal-URL shared-file rejection,kind=previewunit-level rejection,kind=previewinclusive boundary); 2 asserts insend.test.ts(sendMessageLineimage branch preview-cap rejection,pushImageMessagepreview-cap rejection).outbound-media.test.ts(implicitpreviewImageUrl = mediaUrlfallback inresolveLineOutboundMedia); 1 assert insend.test.ts(pushImageMessageimplicit fallback).Secrets and the recipient
userIdwere redacted in any capturedartifact via driver-side substitution; no credentials are committed.