Chrome Extension Development: Fetch non CORS URLs in v3

As browser security improves, many applications have implemented CORS headers to protect against unauthorized access. I'm working on a Chrome extension that needs to check if a certain URL is present on GitHub, before it opens the URL in a new tab. Let's explore what we can do.

The Manifest

Due to CORS headers, it is not possible to perform a fetch request for the HEAD method on a domain that is not included in the headers. However, by utilizing a worker script (in Chrome Extension version 3), it is possible to bypass this restriction. Let's configure the manifest:

{
  "manifest_version": 3,
  "name": "My Extension Name",
  "description": "My Extension Description",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": [ "https://*/*" ],
      "js": ["content.js"]
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["activeTab"]
  "host_permissions": ["https://github.com/*"]
}

It's important to note that the host_permissions array in the manifest file needs to include the URL that the extension will inspect, in this case "github.com" must be added in the array. If you fail to do so, you won't ever be able to fetch the data!

The Background Script

Interaction between the content and background scripts can be achieved through message passing. In our background.js script we'll add an event listener.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.checkIfUrlExists) {
    fetch(request.checkIfUrlExists, { method: "HEAD" })
      .then(response => sendResponse({ status: response.ok }))
      .catch(error => sendResponse({ status: false, error: JSON.stringify(error) }))
  }
  return true
})

The Content Script

With the background script set up, we can use the chrome.runtime.sendMessage API to send messages to it. To make it easier to use async/await in our extension, we'll wrap the call in a Promise:

async function checkIfUrlExists(url) {
  const response = await new Promise(resolve => {
    chrome.runtime.sendMessage({ checkIfUrlExists: url }, resolve)
  })
  return response.status
}

Resolve URLs

My extension needs to check if a certain URL is present on GitHub. Because I don't know the exact name of the file, I'll check multiple possibilities and return the first that has a successful response status:

async function resolveUrl(name) {
  let repo = "https://github.com/keescbakker/my-workload-services"
  let commits = repo + "/commits/master/apps/applicationsets"

  let urls = [
    `${commits}/${name}.yml`,
    `${commits}/${name}.yaml`,
    `${commits}/${name}-service.yml`,
    `${commits}/${name}-service.yaml`,
    `${commits}/${name}-site.yml`,
    `${commits}/${name}-site.yaml`
  ]

  for (let url of urls) {
    if (await checkIfUrlExists(url)) {
      return url
    }
  }

  return null
}

Enjoy!

expand_less