Skip to content

format=match responses are pinned to the first requester's format for a year #61

Description

@feruzm

?format=match is Accept-negotiated at the origin but the response is cached under the URL alone, so the first client to warm a key pins the output format for every later client, for the lifetime of Cache-Control: public,max-age=31536000,immutable.

The origin is correct

Same URL, two Accept headers, straight to the service:

Accept: image/avif,image/webp,image/*,*/*;q=0.8   ->  image/avif    67,770 bytes
Accept: image/jpeg,image/png,image/*;q=0.8        ->  image/png    757,836 bytes

Proper negotiation, and Vary: Accept is set on the response. parseOptions folds Accept into options.format before getImageKey runs, so an AVIF/WebP client resolves to an explicit format and its store key carries it, while a client that negotiated neither stays Match and every such client wants the same bytes. Nothing to fix here.

The CDN does not honour Vary: Accept

Served through the CDN, on a key nobody had requested before, warmed by a modern client first:

1  Accept: avif,webp,...          ->  image/avif  67,770 B  cf=MISS  Vary=Accept
2  Accept: jpeg,png only          ->  image/avif  67,770 B  cf=HIT   Vary=Accept
3  Accept: jpeg,png only (again)  ->  image/avif  67,770 B  cf=HIT   Vary=Accept

Step 2 is a client that explicitly did not offer avif or webp being handed AVIF.

It runs the other way too, and that direction is the expensive one. On an aged key warmed by a non-AVIF client, three different Accept headers all received the same image/png 688,435 bytes where a fresh key renders image/avif 29,010 bytes. That is a 23x bandwidth penalty pinned for a year.

Reproduce with any ?format=match URL at a width that has never been requested, so the key starts cold.

Scale

Sampling distinct sized keys from a single day of origin access logs: 86% wrap .png sources, and 24 of 30 sampled keys were serving a large image/png where a fresh key rendered a small image/avif. So this is the common case for PNG-sourced images rather than an edge case.

(The /p/ token is plain base58 of the source URL, so a key's source extension can be classified offline without fetching anything.)

Why cache invalidation is not the answer

Purging just re-opens the race: whichever client warms the key next re-pins the format for a year. Any fix has to make the cached identity carry the format.

Options

  1. Edge: bucket Accept into the cache key (a small normalisation to something like avif / webp / legacy rather than the raw header, which is too high-cardinality to cache well).
  2. Code: stop depending on Accept at a cacheable URL. Resolve format=match to an explicit format=avif|webp|... URL, so the URL itself is the format identity and the CDN cannot conflate two clients. This is the durable one and it is why I am filing here rather than treating it purely as CDN config.
  3. Do nothing for the correctness direction specifically. A client sending image/* is treated as having told us nothing and keeps the modern format, so the population actually harmed by receiving AVIF is small. The bandwidth direction is the one worth money.

Option 2 interacts with getImageKey's Fit+Match branch and with every caller that builds format=match URLs (@ecency/render-helper's proxifyImageSrc and buildSrcSet), so it is not a small change and is worth designing before starting.

Found while sweeping for stale animated-GIF keys after #58; unrelated to that work and predates it.

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