this.savedUrl.startsWith is not a function
Tags: development, javascript, network, typescript
Published: 2024-04-16 15:13:24

For years we had this dreaded error in our logs: `"this.savedUrl.startsWith is not a function"`. Heaps of time has been spent trying to debug this, along with it's cousins "url.startsWith is not a function." and "url.match is not a function.". Here is the story about how I finally uncovered the causes of this bug, and how I fixed it.

A search for ["this.savedUrl.startsWith is not a function"](https://www.google.com/search?q=%22this.savedUrl.startsWith+is+not+a+function%22) did not yield a single search result. But we had a lot of these in our logs. How could it be that we where the only one affected?

I could not, for the life of me, find "this.savedUrl.startsWith" anywhere in our codebase. The bug, it seemed, weren't ours. And I didn't feel I had anything to go by. At least the stack trace agreed: It claimed that the line of code with the error was outside of any script-file: It claimed the error was on the first line in the index.html file, or in a place called "@user-script:12:1:390" or something similar.


Trying to debugging this, I enhanced our logging, and began collecting the User Agent for every log message. This gave some interesting insights:

1. "url.match is not a function." all come from the iOS snapchat browser
2. "this.savedUrl.startsWith is not a function." all come from Chrome on iOS


I also noticed from the stack trace, that it had something to do with calls to XMLHttpRequest.prototype.open, so I assumed there must have been som non-standard implementation of this method.

What I assumed next, was that we somehow managed to send `undefined` instead of a `string` as URL somewhere. But this didn't make sense either: Our application worked for most of the cases, only these peculiar edge cases seemed to cause trouble.

But either way, the issue was only on iOS devices. I did not have any iOS device for debugging, and the company didn't have any test devices available. So I noted that a developer with an iPhone should take over these bug ticket, and kept developing new features instead of worrying about this old bug.

------

But months went by, without anyone picking up the thread. We regularly had end users complaining about not being able to use our web application, and we still saw these error messages in the logs. So I started investigating some more. I got the bright idea that, instead of searching for the full error message "this.savedUrl.startsWith is not a function", what about only searching for "this.savedUrl.startsWith" instead?

And what I found surprised me: The only place I could find this string, was some forks of the iOS implementation of Chrome.  So I got ahold of another developer who had an iPhone with Chrome, and got him to log into the application. But everything worked as expected. But then I started to investigate the code around "this.savedUrl.startsWith", and saw this was code for translating the page with Google translate!

```javascript
/**
 * Redefine XMLHttpRequest's send to call into the browser if it matches the
 * predefined translate security origin.
 * Only redefines once because this script may be injected multiple times.
 */
if (typeof(XMLHttpRequest.prototype.realSend) == 'undefined') {
  XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.send = function(body) {
    // If this is a translate request, save this xhr and proxy the request to
    // the browser. Else, pass it through to the original implementation.
    // |securityOrigin| is predefined by translate_script.cc.
    if (this.savedUrl.startsWith(securityOrigin)) {
      const length = __gCrWeb.translate['xhrs'].push(this);
      __gCrWeb.message.invokeOnHost({
          'command': 'translate.sendrequest',
          'method': this.savedMethod,
          'url': this.savedUrl,
          'body': body,
          'requestID': length - 1});
    } else {
      this.realSend(body);
    }
  };
}
```

I asked my colleague to turn on automatic translation in his Chrome-for-iOS browser, and there it was! The bug appeared! The whole page crashed and became unusable, if it tried to translate the page during pageload.

After a while, with devtools in Safari, we figured out how to get a breakpoint inside the specific chrome javascript. And sure enough, the url was not a string. **_It was an array!_**

So my assumption that the URL must have been undefined, was wrong. It was an array! And in hindsight this makes sense: If the url was undefined, the error message would have been something completely different, namely: "Uncaught TypeError: Cannot read properties of undefined (reading 'startsWith')".

I felt a bit bad about this: My "undefined" hunch was so wrong, and perhaps I would have solved the bug months ago if I just kept my head straight. But either way, I now know that the URL we tried to fetch was an array, and not a string. Why?

>  If we only had used TypeScript instead of JavaScript, I guess this bug would never have existed in the first place.

By some reasons, a developer decided many many years ago, when first implementing this application, to send in an array to `axios.get` with a single element consisting of the URL, instead of the URL itself. Somehow XMLHttpRequest (and Axios) handles this correctly. But plugins and browsers who intercepts all traffic going through XMLHttpRequest, wasn't quite prepared that we serve an array, instead of a string, to `axios.get`...

More digging into the history of the code, I saw that initially the code looked something like this:

```javascript
axios.get(['/api/', page].join('/'))
```

but in a commit it was rewritten to

```javascript
axios.get(['/api/' + page])
```

And this has, ever since, caused issues for the iOS snapchat browser, Chrome on iOS that translates pages automatically and also browsers with adblock-plugins. If we only had used TypeScript instead of JavaScript, I guess this bug would never have existed in the first place.