Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What should we do about location.replace? #6891

Open
jakearchibald opened this issue Jul 22, 2021 · 5 comments
Open

What should we do about location.replace? #6891

jakearchibald opened this issue Jul 22, 2021 · 5 comments

Comments

@jakearchibald
Copy link
Contributor

@rakina made me aware of a couple of quirks in location.replace, so I went digging, and it's a bit of a mess.

Test 1: Top level replace with new document

Screenshot 2021-07-22 at 11 53 56

This timeline is created by navigating to a page with an iframe, and navigating the iframe twice. The current step is 3.

What happens if location.replace('//new-doc') is called at the top level?

Chrome:

Screenshot 2021-07-22 at 11 57 45

Chrome effectively creates a new history entry for the top level navigable with the same step as the current step (3). The previous step 3 entry (in the iframe) is removed.

Firefox:

Firefox behaves the same as Chrome, but it's a bit buggy with bfcache. If you click back, the iframe is still on //c, but it's just out of sync with browser history. If you click back again, the iframe will navigate to //a, then if you click forward it'll go to //b 😄

Safari:

Like Chrome, except it loses history.state in the top level when iframes are navigated. So:

Screenshot 2021-07-22 at 12 03 23

Test 2: Top level replace with same document

Same initial timeline as before:

Screenshot 2021-07-22 at 11 53 56

But this time location.replace('#foo') is called, making it a same-document replacement.

Chrome:

Screenshot 2021-07-22 at 12 06 02

Chrome replaces the currently active history entry in the top level navigable. State is retained, and the document is copied over. This seems totally inconsistent with test 1.

Firefox:

Screenshot 2021-07-22 at 12 10 08

Firefox creates a new history entry for the top level navigable, and gives it the same step as the current step (3). The iframe entry that also had a step of 3 is removed. However, the iframe isn't navigated, so it still shows //c. If you go back, it still shows //c, if you go back again it shows //a, then if you go forward it shows //b.

Firefox's behaviour is consistent with test 1, but the resulting behaviour shows that it doesn't make sense as a general rule. To make it work, you'd have to navigate the iframe back to //b when the top level calls location.replace, and that just seems like a different type of weird.

Safari:

Screenshot 2021-07-22 at 12 14 42

Like Firefox, Safari creates a new history entry for the top level navigable, and gives it the same step as the current step (3). However, it doesn't remove the iframe entry that also has step 3. This means if you press back, both the iframe and top level navigate at the same time.

This actually feels consistent with test 1, as long as you can get over the fact that nothing was actually replaced.

Test 3: Other iframe replace with new document

Screenshot 2021-07-22 at 12 22 53

This timeline is created by navigating to a page with two iframes, and navigating the first iframe twice. The current step is 3.

What happens if location.replace('//2-b') is called in the second iframe?

Chrome & Safari:

Screenshot 2021-07-22 at 12 26 27

Chrome and Safari create a new history entry for the iframe's navigable, and gives it the same step as the current step (3). It doesn't remove/replace any entry. This means if you press back, both iframes navigate at the same time.

Firefox:

Screenshot 2021-07-22 at 12 34 24

Firefox replaces the current history entry of the iframe.

Test 4: Other iframe replace with same document

Same initial timeline as before:

Screenshot 2021-07-22 at 12 22 53

What happens if location.replace('#foo') is called in the second iframe?

Chrome, Firefox, Safari:

Screenshot 2021-07-22 at 12 37 49

They replace the current history entry of the iframe.

It seems like behaviours here are inconsistent (even within a single browser), or obviously wrong in some cases. I'll try to come up with a 'solution' in a reply.

@jakearchibald
Copy link
Contributor Author

jakearchibald commented Jul 22, 2021

Some ideas to resolve this:


Option 1: The current history entry of the navigable associated with the location object is replaced with a new entry. The new entry has null state.

This takes the thing all browsers do in test 4, what Firefox does in test 3, and what Chrome does in test 2 (except for the state copy), and applies it as a general rule.

If the replacement is same-document, then the document is reused and iframe history state is retained.

I like this because it actually performs a replacement, rather than creating additional history entries, and it doesn't 'steal' history entries from other iframes etc etc. However, since this is pretty different to how browsers behave for test 1 (and browser behave pretty similar to each other in that case), it might not be feasible.


Option 2: A new history entry is created, and is given the current step. The new entry has null state. If the navigable associated with the location object already has a history entry with that step, then that entry is replaced. Entries that are no longer reachable are removed.

Entries become unreachable if they're part of a nested navigable, but if you tried to traverse to their step, a parent would navigate to a different document before they can traverse. For example:

Top level entries: step 1 //, step 2 //#foo
Iframe in //: step 1 //a, step 3 //b, step 4 //c

If the current step is 3, and the top level calls location.replace('//other-page'), the result is:

Top level entries: step 1 //, step 2 //#foo, step 3 //other-page
Iframe in //: step 1 //a, step 3 //b, step 4 //c

The step 3 and 4 items in the iframe are no longer reachable, as the parent would navigate to //other-page where that iframe doesn't exist, so their entries are removed, and the result is:

Top level entries: step 1 //, step 2 //#foo, step 3 //other-page
Iframe in //: step 1 //a

This takes the thing all browsers do in test 1, what Safari does in test 2, and what Chrome and Safari do in test 3, and applies it as a general rule.

This results in cases where a single back click can navigate multiple things at once. That already happens when going back multiple steps, so it doesn't seem like a bad thing.


Option 3: Do option 1 for nested navigables, and option 2 for top-level

This results in the least change for browsers, but does it make sense branching behaviour like this?

Whatever we pick, I'd like to apply the same rule to replaceState if possible.

@jakearchibald jakearchibald changed the title What should we do about history replace? What should we do about location.replace? Jul 22, 2021
@Yay295
Copy link
Contributor

Yay295 commented Jul 22, 2021

I've never had to deal with history and iframes on the same page, but option 1 makes the most sense to me for what should happen if I replaced the current entry. I'd be interested in knowing how many sites this affects, and how many of those sites know that this affects them. I doubt that's something we can really determine, but I imagine it's not a large number.

@annevk
Copy link
Member

annevk commented Jul 22, 2021

I was initially thinking that cross-origin might play a role here and that's why distinguishing between top-level and nested could be reasonable, but after considering it more I'm not sure it would as they could use other history APIs too.

Option 1 seems great indeed. I wonder what @smaug---- thinks.

@rakina
Copy link
Member

rakina commented Jul 22, 2021

I'd like to note that Chrome's behavior in "Test 2: Top level replace with same document" is actually quite new. The previous behavior is the same as for "Test 1: Top level replace with new document" (see comment), where Chrome "replaces" only the current step instead of all the shared entries. I think similarly Test 4's behavior is also new, since we didn't have "shared frame entries" before this change.

So Chrome's behavior was kinda consistent: replace the entry in the current step only (I think this is the same as option 2?). Since this behavior is consistent with other browsers for test 1 & test 2, I'm afraid following option 1 will break some sites that do location.replace() -> history.navigation (then again I don't know if this is common)

That's for location.replace though. I wonder if history.replaceState behaves the same way across browsers. Maybe due to the name that fits Option 1 more? Do you think it makes sense if we have a "change current step" replace and a "change all shared entries" replace?

There's also the same-URL and javascript: replacements which are kinda special and has a separate issue on copying states etc. I haven't really thought about them, but just want to mention them here :)

@jakearchibald
Copy link
Contributor Author

I'm afraid following option 1 will break some sites that do location.replace() -> history.navigation (then again I don't know if this is common

Yeah, I've got a similar worry, but also no data.

There's also the same-URL and javascript: replacements which are kinda special and has a separate issue on copying states etc

I feel ready to give up on history.state entirely 😄. Fwiw, I looked a bit at JS urls in #6798.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants
@jakearchibald @annevk @rakina @Yay295 and others