A ?blur=1 request asks for a ~20px LQIP. When the Sharp encode throws, the handler answers with the untouched original instead, which for an animated GIF cover can be megabytes:
// src/proxy.ts, the blur pipeline
if (options.blur) {
image.resize(20, undefined, { fit: 'inside' }).blur(2).jpeg({ quality: 15, force: true })
contentType = 'image/jpeg'
}
try {
rendered = derive(origin, await runEncode(() => image.toBuffer(), options, clientGoneSignal(ctx)))
} catch (err) {
...
rendered = isFallbackImage(origin) ? origin : passthroughImage(origin.bytes, reason)
contentType = await mimeMagic(origin.bytes)
}
The passthrough is right for a normal variant request: Sharp cannot process the source, browsers are lenient, and the caller wanted that image at some size. It is wrong for a blur request, because the caller wanted a placeholder and a placeholder is never worth more bytes than the image it stands in for. On a 19 MB source the "placeholder" is 19 MB.
Impact today
Rare. Over the last 48h on imh4: 201 sharp.toBuffer() failed, of which 2 were blur requests. So roughly one a day.
It matters a little more from now on. ecency/vision-web#1821 removes the web-side guard that skipped the LQIP layer for .gif cards, because #47 made animated blur real (verified: 16 real animated sources, every ?blur=1 came back 279-899 bytes of image/jpeg). On a source Sharp cannot decode, that card now pulls the original twice rather than once. The card is already serving an unresized original in that case, so it is one extra copy on an image that is broken anyway, which is why it is not worth blocking on. Raised by a review bot on ecency/vision-web#1821.
Suggested shape
Never answer a blur request with origin bytes. The layer is decorative and aria-hidden, sitting behind the real image, so no placeholder at all is a perfectly good outcome and is exactly what the caller got before #47.
⛔ Careful with the obvious alternative: a generated 1x1 or tiny placeholder must NOT be stored or served long-lived under the variant key. A 1x1 placeholder cached immutable, max-age=1y from the origin is what poisoned keys during the Bing index incident, so whatever this returns has to stay out of proxyStore (as passthroughImage already does) and out of a year-long cache.
A
?blur=1request asks for a ~20px LQIP. When the Sharp encode throws, the handler answers with the untouched original instead, which for an animated GIF cover can be megabytes:The passthrough is right for a normal variant request: Sharp cannot process the source, browsers are lenient, and the caller wanted that image at some size. It is wrong for a blur request, because the caller wanted a placeholder and a placeholder is never worth more bytes than the image it stands in for. On a 19 MB source the "placeholder" is 19 MB.
Impact today
Rare. Over the last 48h on imh4: 201
sharp.toBuffer() failed, of which 2 were blur requests. So roughly one a day.It matters a little more from now on. ecency/vision-web#1821 removes the web-side guard that skipped the LQIP layer for
.gifcards, because #47 made animated blur real (verified: 16 real animated sources, every?blur=1came back 279-899 bytes ofimage/jpeg). On a source Sharp cannot decode, that card now pulls the original twice rather than once. The card is already serving an unresized original in that case, so it is one extra copy on an image that is broken anyway, which is why it is not worth blocking on. Raised by a review bot on ecency/vision-web#1821.Suggested shape
Never answer a blur request with origin bytes. The layer is decorative and
aria-hidden, sitting behind the real image, so no placeholder at all is a perfectly good outcome and is exactly what the caller got before #47.⛔ Careful with the obvious alternative: a generated 1x1 or tiny placeholder must NOT be stored or served long-lived under the variant key. A 1x1 placeholder cached
immutable, max-age=1yfrom the origin is what poisoned keys during the Bing index incident, so whatever this returns has to stay out ofproxyStore(aspassthroughImagealready does) and out of a year-long cache.