The Truth Behind Trailing Slashes

There’s been a lot of talk about trailing slashes in URIs lately, and I thought the conversation could use some insight from a browser commercialization expert like myself.

I don’t actually have a strong opinion on the topic, and I don’t think the URI spec does either1, so I thought I’d tell you a story of one of my first Firefox projects instead. This took place around the end of 2019.

Replacing a Jet Engine Mid-flight

I was on the Fission team at Mozilla. Put simply, the goal of the project was to make every unique site in Firefox load in a separate OS process. The linked blog post has more details (and art).

Before Fission, most Gecko/Firefox code was written assuming that a web document would only span a single process. But we were actively breaking that assumption: documents with cross-origin iframes would now need to cross OS-level process boundaries. The browser wasn’t ready to deal with that.

The project took about three years to complete, and was being shipped in a disabled state for most of that time. It was available to turn on, and mostly usable too, but that meant that we were supporting two very distinct configurations… while replacing the core of the browser.

Anyway, a large part of modern browser development is tests. There are, quite literally, 800 million of them. And so, naturally, getting tests to work with Fission was a huge subproject of its own.

Enter BrowserTestUtils.browserLoaded().

This is a helper function used in browser tests to wait for an ongoing load to complete. ripgrep tells me that it’s called over 1300 times, but that doesn’t count functions that call it indirectly, so it’s probably closer to double that.

As soon as we started the “fix all the tests” phase of Fission, it became clear that this function would need to be rewritten. The previous implementation used something called a frame script, but those don’t work in the new process-per-site model. Long story short, the function was rewritten to use JS Window Actors instead.

So I spent a few weeks working on that, and it was fun, but the more interesting learning, at least in my opinion, was in trailing slashes.

…what?

Stick with me.

If you did your homework, then you’d know that browserLoaded() takes an optional wantLoad parameter. It lets the caller wait for a specific load rather than just the first one. The parameter is compared to the URI of every page that loads until a match is found.

Here’s the problem: for any index page, that URI always has a trailing slash2.

Consider the following index.html:

<!DOCTYPE html>    <!-- accessed via http://a.com in the browser -->
<script>
window.addEventListener("load", event => {
  console.log(event.target.documentURI); //=> http://a.com/ <- a slash!
});
</script>

Okay… so why’s that a problem?

I’m getting there. Now consider this browser test:

const URI = "http://b.com";

async function test() {
  const browser = BrowserTestUtils.openNewBrowserWindow();
  BrowserTestUtils.loadURI(browser, URI);
  await BrowserTestUtils.browserLoaded(browser, false, URI);
  ...
}

When you run the test, you’ll see a browser window open, and you’ll see it load b.com, but then… it… just… hangs

I’ll let you figure out why. And make sure you’re subscribed. Next week I’ll be talking about all the fun I had with Punycode.


  1. I’m not actually sure about this. A search for “trailing” in the linked spec and several other specs didn’t turn up much, so this was my conclusion. Please correct me if I’m wrong. ↩︎

  2. I eventually learnt why and how this happens too, but that’s a story for another time. ↩︎