Skip to content

Commit ca3b457

Browse files
committed
fix(line): apply preview cap to implicit-preview image paths
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.
1 parent 37c6fae commit ca3b457

4 files changed

Lines changed: 61 additions & 7 deletions

File tree

extensions/line/src/outbound-media.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,25 @@ describe("resolveLineOutboundMedia", () => {
280280
expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledTimes(1);
281281
});
282282

283+
// Regression: image kind with no explicit previewImageUrl. Downstream
284+
// buildLineMediaMessageObject defaults previewImageUrl to the mediaUrl,
285+
// so a 5 MiB image URL passes the 10 MiB image cap but later fails on
286+
// LINE's 1 MiB preview cap. The resolver must apply the preview cap
287+
// locally on the shared URL.
288+
it("rejects an image with no explicit previewImageUrl when mediaUrl exceeds the preview cap", async () => {
289+
const between = 5 * 1024 * 1024; // under image cap, over preview cap
290+
ssrfMocks.fetchWithSsrFGuard.mockReset();
291+
ssrfMocks.fetchWithSsrFGuard.mockResolvedValueOnce(
292+
buildGuardedHeadResult({ status: 200, contentLength: String(between) }),
293+
);
294+
await expect(
295+
resolveLineOutboundMedia("https://example.com/implicit-preview.jpg", {
296+
mediaKind: "image",
297+
}),
298+
).rejects.toThrow(/LINE preview media must be 1048576 bytes \(got 5242880 bytes/);
299+
expect(ssrfMocks.fetchWithSsrFGuard).toHaveBeenCalledTimes(1);
300+
});
301+
283302
// T11
284303
it("rejects a video URL whose HEAD reports a payload larger than the LINE video cap", async () => {
285304
const oversized = LINE_OUTBOUND_MEDIA_MAX_BYTES.video + 1;

extensions/line/src/outbound-media.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,12 @@ export async function resolveLineOutboundMedia(
196196
(opts.trackingId?.trim() ? "video" : undefined) ??
197197
detectLineMediaKindFromUrl(trimmedUrl) ??
198198
"image";
199-
if (previewImageUrl && previewImageUrl === trimmedUrl) {
200-
// Same URL serves as both originalContentUrl and previewImageUrl;
201-
// dedupe to one HEAD probe but evaluate against the stricter preview cap.
199+
if (mediaKind === "image" && (!previewImageUrl || previewImageUrl === trimmedUrl)) {
200+
// For image kind, buildLineMediaMessageObject defaults
201+
// previewImageUrl to mediaUrl when no explicit preview is supplied
202+
// (and an explicit same-URL preview is handled identically). The
203+
// shared URL must therefore satisfy the stricter preview cap; one
204+
// HEAD probe covers both LINE-side validations.
202205
await precheckLineOutboundMediaSize(trimmedUrl, "preview");
203206
} else {
204207
await precheckLineOutboundMediaSize(trimmedUrl, mediaKind);

extensions/line/src/send.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,36 @@ describe("LINE send helpers", () => {
550550
expect(replyMessageMock).not.toHaveBeenCalled();
551551
});
552552

553+
// Regression: pushImageMessage with no explicit previewImageUrl.
554+
// createImageMessage defaults previewImageUrl to originalContentUrl, so a
555+
// 3 MiB original URL passes the 10 MiB image cap but later fails on
556+
// LINE's 1 MiB preview cap. pushImageMessage must apply the preview cap
557+
// locally on the shared URL, with a single HEAD probe (dedupe preserved).
558+
it("pushImageMessage rejects when originalContentUrl with no explicit preview exceeds the preview cap", async () => {
559+
const between = 3 * 1024 * 1024; // under image cap, over preview cap
560+
fetchWithSsrFGuardMock.mockReset();
561+
fetchWithSsrFGuardMock.mockResolvedValueOnce({
562+
response: new Response(null, {
563+
status: 200,
564+
headers: new Headers({ "content-length": String(between) }),
565+
}),
566+
finalUrl: "https://example.com/implicit-preview.jpg",
567+
release: async () => undefined,
568+
});
569+
570+
await expect(
571+
sendModule.pushImageMessage(
572+
"line:user:U777",
573+
"https://example.com/implicit-preview.jpg",
574+
undefined,
575+
{ cfg: LINE_TEST_CFG },
576+
),
577+
).rejects.toThrow(/LINE preview media must be 1048576 bytes \(got 3145728 bytes/);
578+
579+
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(1);
580+
expect(pushMessageMock).not.toHaveBeenCalled();
581+
});
582+
553583
// Preview-cap regression — pushImageMessage must reject when an explicit
554584
// preview URL exceeds the LINE 1 MiB preview cap, and pushMessage must not
555585
// be reached even though the original content URL passes the 10 MiB cap.

extensions/line/src/send.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,15 @@ export async function pushImageMessage(
403403
if (previewImageUrl) {
404404
await validateLineMediaUrl(previewImageUrl);
405405
}
406-
if (previewImageUrl && previewImageUrl === originalContentUrl) {
406+
if (!previewImageUrl || previewImageUrl === originalContentUrl) {
407+
// createImageMessage defaults previewImageUrl to originalContentUrl
408+
// when no explicit preview is supplied (and an explicit same-URL
409+
// preview is handled identically). The shared URL must therefore
410+
// satisfy the stricter preview cap.
407411
await precheckLineOutboundMediaSize(originalContentUrl, "preview");
408412
} else {
409413
await precheckLineOutboundMediaSize(originalContentUrl, "image");
410-
if (previewImageUrl) {
411-
await precheckLineOutboundMediaSize(previewImageUrl, "preview");
412-
}
414+
await precheckLineOutboundMediaSize(previewImageUrl, "preview");
413415
}
414416
return pushLineMessages(to, [createImageMessage(originalContentUrl, previewImageUrl)], opts, {
415417
verboseMessage: (chatId) => `line: pushed image to ${chatId}`,

0 commit comments

Comments
 (0)