Skip to content

Preload links for image URLs containing commas can be erroneously percent-encoded #1906

@westonruter

Description

@westonruter

Bug Description

On my site I'm using the Jetpack Image CDN (Photon). The LCP IMG element on the homepage is rendered by WordPress as:

<img
  width="1200"
  height="675"
  src="https://weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond-1200x675.png"
  class="attachment-post-thumbnail size-post-thumbnail wp-post-image"
  alt="Boosting Performance with Optimization Detective"
  style="width: 100%; height: 100%; object-fit: cover"
  decoding="async"
  fetchpriority="high"
  srcset="
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1200%2C675&amp;ssl=1  1200w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=300%2C169&amp;ssl=1    300w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=700%2C394&amp;ssl=1    700w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=768%2C432&amp;ssl=1    768w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1536%2C864&amp;ssl=1  1536w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=2048%2C1152&amp;ssl=1 2048w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1980%2C1114&amp;ssl=1 1980w,
    https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=150%2C84&amp;ssl=1     150w
  "
  sizes="(max-width: 1200px) 100vw, 1200px"
/>

When Optimization Detective with Image Prioritizer runs, it identifies that this is the LCP element and it adds a preload link with fetchpriority=high, as expected:

<link
  data-od-added-tag
  rel="preload"
  fetchpriority="high"
  as="image"
  href="https://weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond-1200x675.png"
  imagesrcset="https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1200%2C675&amp;ssl=1 1200w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=300%2C169&amp;ssl=1 300w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=700%2C394&amp;ssl=1 700w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=768%2C432&amp;ssl=1 768w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1536%2C864&amp;ssl=1 1536w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=2048%2C1152&amp;ssl=1 2048w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=1980%2C1114&amp;ssl=1 1980w, https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=150%2C84&amp;ssl=1 150w"
  imagesizes="(width &lt;= 480px) 300px, (480px &lt; width &lt;= 600px) 455px, (600px &lt; width &lt;= 782px) 644px, (782px &lt; width) 645px"
  media="screen"
/>

Nevertheless, when I look at the Chrome DevTools console I see an unexpected warning:

Warning

The resource was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate as value and it is preloaded intentionally. The resource https://i0.wp.com/weston.ruter.net/wp-content/uploads/2025/02/weston-ruter-beyond.png?resize=768,432&ssl=1 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate as value and it is preloaded intentionally.

When I look at the network panel I see that there are two copies of this image being loaded:

Image

One of the URLs is getting the comma percent-encoded (the one in the LINK tag) and Chrome isn't understanding the URLs are the same. This seems like a bug in Chromium because these two URLs should be normalized to be the same, I should think:

In any case, the problem is that there is a percent-encoding which was originally present in the URL which was stripped out:

private function encode_url_for_response_header( string $url ): string {
$decoded_url = urldecode( $url );
// Encode characters not allowed in a URL per RFC 3986 (anything that is not among the reserved and unreserved characters).
$encoded_url = (string) preg_replace_callback(
'/[^A-Za-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]/',
static function ( $matches ) {
return rawurlencode( $matches[0] );
},
$decoded_url
);
return esc_url_raw( $encoded_url );
}

This originates in #1802 and #1866.

The issue is not limited to percent encodings in the query parameters either. If I try to percent-encode the "j" in the .jpg file extension as .%6Apg, I get the same issue. However, if I add a query parameter with percent-encoding for text which must be percent-encoded (e.g. Arabic text), then I do not experience this issue. For example:

<figure>
  <img
    src="https://upload.wikimedia.org/wikipedia/commons/8/8d/American_bison_k5680-1.jpg?arabic=%D8%A7%D9%84%D8%A8%D9%8A%D8%B3%D9%88%D9%86&hebrew=חנות"
    alt="Bison"
    style="max-width: 100%; height: auto; aspect-ratio: 1270 / 828"
  />
</figure>

Results in this LINK tag:

<figure>
  <img
    src="https://upload.wikimedia.org/wikipedia/commons/8/8d/American_bison_k5680-1.jpg?arabic=%D8%A7%D9%84%D8%A8%D9%8A%D8%B3%D9%88%D9%86&hebrew=חנות"
    alt="Bison"
    style="max-width: 100%; height: auto; aspect-ratio: 1270 / 828"
  />
</figure>

And this HTTP Link response header:

Link: <https://upload.wikimedia.org/wikipedia/commons/8/8d/American_bison_k5680-1.jpg?arabic=%D8%A7%D9%84%D8%A8%D9%8A%D8%B3%D9%88%D9%86&hebrew=%D7%97%D7%A0%D7%95%D7%AA>; rel="preload"; fetchpriority="high"; as="image"; media="screen and (782px < width)"

Nevertheless, only entry URL shows up in the Network log:

Image

So it appears that we need to (1) stop doing urldecode() and then (2) only encode the characters that must be encoded (while leaving any percent encodings in the URL intact).

Steps to reproduce

  1. Activate the Image Prioritizer plugin.
  2. Add a Custom HTML block with the following contents:
<figure>
  <img
    src="https://upload.wikimedia.org/wikipedia/commons/8/8d/American_bison_k5680-1.%6Apg?foo=bar%2Cbaz"
    alt="Bison"
    style="max-width: 100%; height: auto; aspect-ratio: 1270 / 828"
  />
</figure>
  1. Visit the frontend to trigger URL Metric collection.
  2. Reload the page in which a preload link has been added.
  3. Look at Network panel to see two almost-identical image URLs being loaded with high priority, when there should only be one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    [Plugin] Image PrioritizerIssues for the Image Prioritizer plugin (dependent on Optimization Detective)[Plugin] Optimization DetectiveIssues for the Optimization Detective plugin[Type] BugAn existing feature is broken

    Type

    No type

    Projects

    Status

    Done 😃

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions