Verify if a url links to an image (without relying on regex)

#javascript

one of the rare cases that I need to work with MIME types in frontend web development

In a side project I built, I had an input field that let users to pass urls that are supposed to link to image files, and I wanted to add client-side validation for that.

I started with the most obvious solution: regular expressions.

Regex#

A list of common image file types that are supported by most browsers can be found on this MDN page. I can check if the url ends with one of these file extensions:

function isImgUrl(url) {
  return /\.(jpg|jpeg|png|webp|avif|gif)$/.test(url)
}

The most obvious drawback with this approach is that I have to list out every single possible image file extension in the regex.

Also, it is based on a false premise: all image urls must have the correct file extension appended at the end. The reality is, on the web, file extensions don't really matter. For example, this url https://avatars.githubusercontent.com/u/33640448?v=4 links to my Github profile picture, but it doesn't end up with any image file extension. The current solution fails to cover this edge case.

Image's onload event#

Another approach is to create an image tag <img > dynamically in JavaScript and assigning the url to its src attribute. If the onload event fires off, then that means the browser can correctly decode the image data linked by the url. Thus we know it is indeed an image url.

function isImgUrl(url) {
  const img = new Image();
  img.src = url;
  return new Promise((resolve) => {
    img.onload = () => resolve(true);
    img.onerror = () => resolve(false);
  });
}

This approach works with urls that don't have file extensions attached.

function isImgUrl(url) {
  const img = new Image();
  img.src = url;
  return new Promise((resolve) => {
    img.onerror = () => resolve(false);
    img.onload = () => resolve(true);
  });
}

const urls = [
  'https://avatars.githubusercontent.com/u/33640448?v=4',
  'https://httpbin.org/image/webp',
  'https://upload.wikimedia.org/wikipedia/commons/a/a3/June_odd-eyed-cat.jpg'
];

Promise.all(urls.map((url) => isImgUrl(url))).then(console.log); // [true, true ,true]

However, by assigning the url to the src attribute of an image tag, the browser kicks off a Get request to download the whole image when we don't plan to actually render anything yet. This solution doesn't scale well if we have hundreds of urls to check.

Sending HEAD requests and only check for the MIME type#

Turns out there is a way to find out if a url links to an image file without incurring the cost of downloading. That is, to send a HEAD request to get the MIME type from the Content-Type header.

As we have seen previously, file extensions are not a necessary part of a url. Instead, browsers use MIME Type to interpret the contents of requested files. When a image file comes off the network, browsers look at the response's Content-type header for its MIME type and invoke the appropriate decoder to decompress the image file.

Different image formats all start with the image MIME type, along with different subtypes. For example, the MIME type of a jpeg file is image/jpeg, and for a png file it is image/png. Therefore, we only need to check if the MIME type starts with image:

function isImgUrl(url) {
  return fetch(url, {method: 'HEAD'}).then(res => {
    return res.headers.get('Content-Type').startsWith('image')
  })
}

Now without having to incur the overhead of a full response payload, we get to know if a given url links to an image file.

Note that this is still not a production grade solution. There a few noticeable drawbacks to it: 1. it doesn't work when the request is rejected by CORS 2. it doesn't work with images formats that are not supported by browsers.