whatwg / html

HTML Standard
https://html.spec.whatwg.org/multipage/
Other
7.96k stars 2.6k forks source link

Which navigations should add/replace new history entry after initial empty document? #6491

Closed rakina closed 2 years ago

rakina commented 3 years ago

I recently tested out how navigations after initial empty document affect history in various browsers and found some interesting cases on how session history is updated after a navigation from an initial empty document. Interestingly, the behavior also differs depending on whether the initial empty document is on an iframe vs opened window, although they both go through "create a new browsing context" to create the initial empty document..

iframe loaded with unset src or about:blank

Repro at https://rakina.github.io/aboutblankiframe.html, https://rakina.github.io/unsetiframe.html

Navigate to Chrome Firefox Safari
about:blank replaces replaces appends
about:blank#foo replaces appends appends
title1.html (same-origin page) replaces appends replaces
example.com (cross-origin page) replaces appends replaces
about:blank#foo => title1.html replaces, replaces appends, appends appends, appends
about:blank#foo => example.com replaces, replaces appends, appends appends, appends

"replaces" means the history.length stays the same and we can't navigate back to the previous URL, while "appends" means history.length is increased by and 1 and we can navigate back.

Chrome seems to always replace the entry for the initial empty document after every navigation (even same-document/fragment navigations).

On Safari, after any navigation that appends (even to "about:blank#foo"), the "replace" behavior is gone. So if you go from initial empty document => about:blank#foo => example.com, you'll end up with 3 history entries.

Additionally, if you navigate to about:blank#foo then to example.com then go back in Firefox, the resulting about:blank#foo page will have a different origin than the main frame.

window.open to unspecified URL or about:blank

Repro at https://rakina.github.io/windowopenblank.html

Navigate to Chrome Firefox Safari
initial state history.length == 0 history.length == 0 history.length == 1
about:blank replaces, length++ replaces, length++ replaces, length++
about:blank#foo replaces, length++ replaces, length++ replaces, length++
title1.html (same-origin page) replaces, length++ replaces, length++ replaces, length unchanged
example.com (cross-origin page) replaces, length++ replaces, length++ replaces, length unchanged
about:blank#foo => about:blank => go back about:blank, same-origin with opener about:blank, different-origin with opener about:blank, different-origin with opener
example.com => about:blank length == 2 length == 2 length == 2

The replacement behavior seems to be quite consistent in this case which is nice.

In Chrome & Firefox, the opened window starts with history.length of 0, while in Safari it starts with history.length of 1. Chrome & Firefox generally behave the same way with history.length here, and Safari mostly follows too (just starts with a different a value), though there are some weird cases (then again, history.length is generally unreliable so I guess this doesn't really matter?)

Again, there's some interesting cases with origin. I have no idea how the origin is calculated, just noting some interesting findings.

My questions:

My suggestions:

Would love to hear people's thoughts. cc @domenic @jakearchibald @csreis @annevk @smaug---- @cdumez

Apologies if this is already discussed somewhere else, I searched for existing related issues and they don't seem to talk about this case specifically and also might be a bit outdated (https://github.com/whatwg/html/issues/546, https://github.com/whatwg/html/issues/490).

jakearchibald commented 3 years ago
  • Do we have special cases in the spec for navigations after the initial empty document? (I tried looking around the 'navigate' algorithm and browsing context creation, but I don't think I see any)

Not that I've seen

I've been trying to find this for a while too! Looks like it's step 5 https://html.spec.whatwg.org/multipage/browsing-the-web.html#initialise-the-document-object

  • Are we supposed to keep the opener's origin when navigating away then back to initial empty documents?

As a developer, that's the behaviour that would make most sense to me.

  • The window.open replacement behavior seems to be quite consistent, so it probably makes sense to change the iframe behavior to follow it? This means Safari & Firefox should change their behavior to follow Chrome.

I agree, then it isn't down to timing. If you append <iframe src="https://example.com">, all browsers add an iframe with about:blank then replacement-navigate it to example.com. It seems to make sense to make this always-replace.

It feels like the issue discussed in https://github.com/whatwg/html/issues/1191 goes away with the always-replace-initial-nav behaviour. Maybe that means we can all fix the bug where child contexts don't navigate if their parent is also navigated https://github.com/whatwg/html/issues/5767#issuecomment-775777376.

That makes the most sense to me. I was really surprised to hear it starts at 0. Just to confirm: You're saying it should start at 1, and then remain 1 after navigation (since the navigation is "replace").

domenic commented 3 years ago

Do we have special cases in the spec for navigations after the initial empty document? (I tried looking around the 'navigate' algorithm and browsing context creation, but I don't think I see any)

I'm not sure this is what you mean, but historyHandling gets set to "replace" in a few cases:

The spec implies a Window's associated Document can change if it was an initial empty document, but I can't find anything that changes the associated document. Am I missing something?

Right, as Jake pointed out, this is due to https://html.spec.whatwg.org/#initialise-the-document-object step 5/6. In the initial about:blank case, we "do nothing" (step 5). In all other cases, we create a new window.

However, actually changing the "associated Document" seems to be broken... at least, I can't find where that happens. It seems like it should happen immediately. I wonder if we broke this recently... /cc @domfarolino

Are we supposed to keep the opener's origin when navigating away then back to initial empty documents?

This makes sense to me, but I suspect this was never really considered. This is the case you're triggering via about:blank#foo, right? I suspect people just didn't think of that.

The window.open replacement behavior seems to be quite consistent, so it probably makes sense to change the iframe behavior to follow it? This means Safari & Firefox should change their behavior to follow Chrome.

+1. Also, always doing replace matches the spec better I believe, and seems simpler to understand.

For history.length in the window.open case, perhaps it makes sense to start with 1 since we do add the initial empty document to the session history.

My understanding is that web developers sometimes use history.length > 0 to test if history.back() will do something useful. Anything that preserves that connection works for me.

Apologies if this is already discussed somewhere else, I searched for existing related issues and they don't seem to talk about this case specifically and also might be a bit outdated (#546, #490).

Do you think this issue covers all the issues discussed in those, and we can close them and roll them into here?

domenic commented 3 years ago

However, actually changing the "associated Document" seems to be broken... at least, I can't find where that happens. It seems like it should happen immediately. I wonder if we broke this recently... /cc @domfarolino

I think it actually happens via history traversal, some steps later. E.g. if you look at the page load processing model for HTML files, it first creates the document, and then "before any script runs", it calls "update the session history with the new page", which will call "traverse the history", which will eventually call "set the active document".

I'm going to add a note to make this clearer since it's so easy to miss.

Yay295 commented 3 years ago

My understanding is that web developers sometimes use history.length > 0 to test if history.back() will do something useful.

This seems like a broken test to me. Without knowing anything about how browser history works, I wouldn't expect history.back() to work unless history.length > 1. It seems like testing against 0 would only work in Safari anyways. Did you mean history.length > 1 instead?

domenic commented 3 years ago

Yes, I apologize for the typo.

rakina commented 3 years ago

I'm not sure this is what you mean, but historyHandling gets set to "replace" in a few cases:

https://html.spec.whatwg.org/#concept-form-submit step 20 (form submit) https://html.spec.whatwg.org/#window-open-steps step 14.6.1 (window.open) https://html.spec.whatwg.org/#initialise-the-document-object step (iframes... maybe also overlaps window.open? Hmm.)

Ah thanks, looking at the setters for "replace" I also found some other cases:

However some callers to navigate does not do the "replace", which sounds bad:

Overall having the caller to "navigate" set the "replace" bit for this case seems error-prone, and we might forget if we add more callers. Do you think it makes sense to actually make the "replace on every navigation when the document is the initial empty document" step part of "navigate"?

I think it actually happens via history traversal, some steps later. E.g. if you look at the page load processing model for HTML files, it first creates the document, and then "before any script runs", it calls "update the session history with the new page", which will call "traverse the history", which will eventually call "set the active document".

I'm going to add a note to make this clearer since it's so easy to miss.

Awesome, thanks!

Are we supposed to keep the opener's origin when navigating away then back to initial empty documents?

As a developer, that's the behaviour that would make most sense to me.

This makes sense to me, but I suspect this was never really considered. This is the case you're triggering via > about:blank#foo, right? I suspect people just didn't think of that.

Yeah, it seems like Safari & Firefox doesn't treat about:blank in fragments as about:blank (at least in some cases). I think they should (like Chrome), and I think this aligns with the fetch spec for about:blank (it checks just the path)

For history.length in the window.open case, perhaps it makes sense to start with 1 since we do add the initial empty document to the session history.

That makes the most sense to me. I was really surprised to hear it starts at 0. Just to confirm: You're saying it should start at 1, and then remain 1 after navigation (since the navigation is "replace").

My understanding is that web developers sometimes use history.length > 1 to test if history.back() will do something useful. Anything that preserves that connection works for me.

Yeah, I'm proposing we always start with 1, and if we do "replace" (which in my proposal is always the case) it will stay at 1. After that if we're not on the initial empty document anymore, we should update it accordingly (increase on append, stay the same on replace/reload).

Apologies if this is already discussed somewhere else, I searched for existing related issues and they don't seem to talk about this case specifically and also might be a bit outdated (#546, #490).

Do you think this issue covers all the issues discussed in those, and we can close them and roll them into here?

I think we cover #546 in this issue - "The iframe keeps its content" should be the behavior for case 3 & 4 there if we go with the "always replace" proposal.

I'm not sure about #490 - it seems like it's referencing spec text that no longer exists. I think @ArthurSonzogni looked into the synchronous about:blank navigation a while ago (see doc) and found that Firefox doesn't support it while Chrome does. Ideally we'd like to follow Firefox, but looks like there are some dependencies to that behavior in Chrome at least...

domenic commented 3 years ago

Overall having the caller to "navigate" set the "replace" bit for this case seems error-prone, and we might forget if we add more callers. Do you think it makes sense to actually make the "replace on every navigation when the document is the initial empty document" step part of "navigate"?

Hmm, I see, good catch. I guess the current spec's philosophy is that the "initial navigation" should be replace, i.e. the navigation to foo if you're doing <iframe src="foo"> or window.open("foo"). Whereas you're advocating for, any navigation that takes you from the initial about:blank should be a replace.

I suspect the latter is more correct, and in that case, indeed a spec change like you suggest would be good. But of course we should double-check with web platform tests, if there aren't any already.

BTW, we should try to revive https://github.com/whatwg/html/pull/4691 so we can have a clearer spec idea of what the initial about:blank Document actually is.

annevk commented 3 years ago

I'm pretty sure you are influencing the results in Firefox by manipulating the contents of the initial about:blank document. bz explained this to me at some point and the general idea is that we attempt to determine if there was a document of sorts there as the user might wish to navigate back to it. (I think this is discussed in some issue against this repository as well.) I personally think it would be fine to remove this behavior. I'll see if others agree.

rakina commented 3 years ago

Hmm, I see, good catch. I guess the current spec's philosophy is that the "initial navigation" should be replace, i.e. the navigation to foo if you're doing