I recently fixed a very surprising bug on a website that only happened in Safari (both desktop and mobile). I’ve searched the Internet, but can’t find anything written about the root cause, so I’m asking here.
If you fire off several XMLHttpRequest
s to the same URL at the same time in Safari, only one of actually gets sent to the server. All the XHRs are then resolved with the same response. That’s problematic if the URL returns a unique response every time, such as a UUID. Firefox and Chrome both send all the requests.
Here’s some test code. Paste it in the console on any site. Check the Network tab in the devtools and you’ll see that Safari only sends one request.
Promise.all(
Array.from(
{ length: 10 },
() =>
new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => resolve(xhr.response));
xhr.open("GET", "/random");
xhr.send();
})
)
).then(console.log, console.error);
(It also happens when using a good old for
loop, so it’s not the Promise
s fault.)
POST and PUT requests are also affected, as long as you don’t send a body (xhr.send("some body")
).
When using fetch
instead of XMLHttpRequest
, the issue does not occur and Safari makes 10 requests:
Promise.all(Array.from({ length: 10 }, () => fetch("/random"))).then(
console.log,
console.error
);
Given that Firefox, Chrome and fetch
makes all the requests, is this a bug in Safari? Have anyone else ever encountered it?
A quick workaround is to make all URLs unique via query parameters:
Promise.all(
Array.from(
{ length: 10 },
(_, i) =>
new Promise(resolve => {
let xhr = new XMLHttpRequest();
xhr.addEventListener("load", () => resolve(xhr.response));
xhr.open("GET", `/random?i=${i}`);
xhr.send();
})
)
).then(console.log, console.error);
I’ve also tried putting delays (setTimeout
, 1 to 100 ms) between requests, with varying results: Everything from 1 to 10 requests are made.
Now, you might wonder why I make several XHRs at the same time. It’s for an image upload. The user chooses a couple of images and the hits “Upload”. One request is fired per image to an API endpoint that returns an S3 pre-signed URL, which the image then is uploaded to. Due to Safari’s “skipping” behaviour all the images got the same S3 pre-signed URL, so they all got uploaded to the same place, overwriting each other.
(I’m using XMLHttpRequest
instead of fetch
because Elm uses XMLHttpRequest
under the hood.)
The quickfix was to add a query parameter to the URLs to make them unique (as mentioned above). I could also change the API endpoint to return several S3 pre-signed URLs at once. Either way, I’m still interested in learning more about this surprising behavior in Safari, and wanted to share my findings in case it helps anyone else.