Skip to content

A failed blur encode answers a 20px LQIP request with the full original #60

Description

@feruzm

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions