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 whether a certain URL exists on GitHub, before it opens the URL in a new tab. Let's explore what we can do.

The Manifest

Content scripts run in the context of the page and are subject to its CORS restrictions. Fetching a cross-origin URL, even with the HEAD method, is blocked unless the target server includes permissive CORS headers. A service worker, however, runs in a privileged extension context and can bypass those restrictions when the right host permissions are set. 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 be able to fetch the data at all!

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 whether a certain URL exists 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 brightness_auto