-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Changing the session history spec to converge on historical & modern browser behaviour #5767
Comments
Moving my complaints about |
This is great, we've been struggling with these compat issues at Surfly a few years back. Is there something I could help with? Perhaps contributing a few WPT tests? Also, I wonder if these points are worth extra research:
|
This would address most of the things we wanted on #5350 (comment)! One thing not covered yet: restoring states/relationships (names, scripting relationships, whether it's an auxiliary browsing context/not, WindowProxy) when we've already deleted a browsing context and we're navigating back to it. I think the idea proposed at the time was to save the states/relationship within the history entry. Do you think that will work? Or we can make browsing contexts live forever, if there's nothing preventing us from doing that? (In Chrome, I think that's how it's implemented - we save SiteInstance in FrameNavigationEntry so the states/relationships etc are always restorable) |
Ohh, that would be great. It might be too soon right now, but I'll let you know.
As far as I know they create separate history stacks.
I think there are some cases where the history API is a no-op if it would impact something outside of the context it was called from, eg if it's called from a sandboxed iframe. |
I've started playing around with ideas for that. Instead of "target browsing context" I think it'll be "restorable browsing context", where it can be a browsing context, or something that can be used to recreate the browsing context, and ensure that the recreated browsing context is used across all the history entries that previously shared a context. In my sketches this was just a unique ID, but I can make it something that includes other state like names.
I haven't read up on this yet, but I'll see if I can include it.
I haven't thought too much about this. Is my mental model here correct:
Is behaviour in this case agreed? As in, should the window proxy become reconnected to the new browsing context?
I assumed that making browsing contexts live forever would be problematic for memory, but maybe it's light enough once all the active parts are removed? Especially since it'll no longer own session history. |
For names specifically, I just checked, it looks like whether we want to restore that or not is still unclear. Arthur Hemery (@ahemery?) from COOP team (cc @ParisMeuleman @clamy) wrote a doc on the current handling of window.names in chrome, and it looks like we actually don't store it on history entries. There's also a related bug (quite old). Edit: From #5679, though, it looks like Firefox and Safari are already restoring browsing context names?
Yes, I think if we the next page is isolated with COOP we would change to a new browsing context (and browsing context group), and it won't be an auxilliary BC (as it isn't related to another top level BC anymore). When restoring the old page, its browsing context should still be an auxilliary BC (maybe should be tied to whether or not we restore WindowProxy below, though) Some parts where the bit is important: script-closable and not clearing name.
Hmm right. In Chrome (from local testing): Case #1 (BC is gone, BCG stays because there's another BC alive)
Case #2 (BCG & BCs are gone):
I haven't looked at the exact restoration logic in Chrome, maybe will do so soon and report back, or @csreis can comment :) Also cc @annevk @mystor who commented that maybe this is not how that would work in Firefox #5350 (comment)
Yeah I guess we kinda only want to make it possible to do "if we restore entry for B, and B's window.opener was A and it's still around we should restore the WindowProxys between them".
|
This is mostly an editorial change, which introduces explicit <dfn>s and links for the various items of a session history entry. However, it also includes some bugfixes and substantial clarifications. Notable ones: * Removes the mention of "form data" as being part of a session history entry. This was never referenced elsewhere in the spec. It may have something to do with form resubmission upon history traversal (which seems to be unspecified), or maybe it was subsumed into "persisted user state". For now it's best removed. * Removes a paragraph saying that entries with discarded documents should act as if the documents were not discarded. This seems totally wrong; re-creation of documents is handled explicitly in the spec. * Fixes the "URL and history update steps" to not create document-less session history entries. * Symmetrizes and more tightly couples scroll position saving/restoration and saving/restoration of other, implementation-defined, persisted user state. * Rewords all the discussion of contiguous, document-sharing session history entries for improved clarity. * Updates all history-related IDL constructs to modern style, including adding a "state" value for history.state to return, and creating "shared history push/replace state steps" for history.pushState() and history.replaceState() to call into. * Reduces some of the implicit variable-passing, notably for the "navigate to a fragment" algorithm. This helps set the stage for #5767, but does not yet make any of the changes proposed there.
I'm trying to figure out the window proxy stuff, and it seems a little different in browsers: Test 1
All browsers (Chrome, Safari, Firefox) deliver the post-message (step 5) and error when trying to access the document (step 6). This seems as expected. Test 2
Chrome: Fails to deliver message (step 5). Cross-origin error in step 6. Fails to close in step 7. Chrome's behaviour here seems more secure. Since the navigation is manual, it seems like the new page should be cut-off from its opener. However, it isn't entirely cut-off, since you get the cross-origin error in step 6. Test 3
Chrome: Fails to deliver message in step 5. The initial It makes sense to have ProposalWhen the user navigates the tab to a new history entry using a method outside of the document (location bar, bookmarks etc), the new page should not have ties to the opener. This could be achieved by creating a new browsing context for the history entry, which means the behaviour of tests 2 & 3 should be the same. The window proxy would continue to be owned by the browsing context, although maybe we should prevent access to things like the document while the target page is in bfcache? I plan to store the browsing context in the session history entry, so when the browser goes back it can reload the page in the right context. |
A quick note between meetings...
I'm not sure there's a security issue here, per se: postMessage is about message passing, which should not be a security problem to allow cross-origin. What's happening here, I'm pretty sure, is that Chrome did a BCG swap on URL bar navigation, so that puts the window into a separate browsing context group, which conceptually means you can't message it. That is, normally when you have two different windows in different BCGs, there's no way for them to get a handle to each other, so messaging is impossible. With Chrome's BCG swap on URL bar navigation, that invariant no longer holds; we have windows with handles to each other, but in different BCGs. I guess this manifests in Chrome by having postMessage() fail, in a not-specced way. I slightly favor the Safari and Firefox behavior here, in that I like the idea that if you have a handle to a window then you can message it. But I don't feel strongly, and if Chrome implementers have implementation concerns then that should probably outweigh my instincts. Plus, there might be a theoretical reason why allowing cross-BCG messaging is a bad idea. I haven't processed your test 3 yet, or your proposal... Will do after the next meeting block. |
I guess I'm more concerned about Tab1 still being able to navigate Tab2.
I think it's something different, else it'd behave the same as test 3.
Yeah, I don't have strong feelings either. It'd be nice if tests 2 & 3 behaved the same, but if they don't then I'll need to figure out another place for window proxy to live, and how it works when going across browsing contexts. |
Good point; that should not be allowed indeed, IMO. I didn't see that tested though?
Oh, interesting, and good catch. In particular it fails to deliver the message in both cases (so probably there's a reason for that?). The difference is whether I do agree they should behave the same. If your hypothesis about the undefined vs. initial example.com document different being bfcache-related is correct, I'm unsure what to think there... bfcache should not cause observable changes like that, IMO, especially not unspecified ones like having I also agree that reconnecting on going back would make sense, if
This sounds good to me personally, and seems to align with what I understand of #5350. Although, it sounds like we'd still need to nail down the behavior of actual property access like I'm really looking forward to hearing from @rakina and @annevk on this subject! |
Yeah, that's fair, I'm making an assumption. Here we go! Test 4
Chrome: Does not navigate.
Yeah, that seems fair. I need to figure out if the window proxy is actually associated with browsing context. As in, if a reference to the same window is made in two iframes, are the returned objects ===. I'd be surprised if they are, but I've been surprised more often than not while looking at this stuff. |
Test 2: The user initiating navigation through the address bar should result in a BCG swap (and maybe even a browsing session swap, though the user-facing back button should still work). See #2635. A BCG swap would result in closure of the popup, which should not make the document appear cross-origin, so that is definitely odd behavior and something to address, I think. Test 3: COOP results in a BCG swap and what Firefox does here are the results I would expect given the specification. The one thing we left undecided is the back button / Proposal: Note that in either case creating a new browsing context is not enough, you need a new browsing context group. And given that it's new, the WindowProxy object will simply point to the old closed one. (There should never be cross-BCG references other than through origin-based storage channels such as Test 4: This should behave the same as trying to navigate a closed tab, which I think would fail. To make the test applicable to Firefox it would help to do this after COOP. As noted above we still need to expand and standardize the number of cases where a COOP-like thing happens. WindowProxy: Yes, they should be 1:1 whenever this can be observed. Across sites you get "remote WindowProxy" objects that serve as a proxy for the WindowProxy due to site isolation, but you can mostly not observe that from script (though there are some cases we have issues on and we should tidy things in the specification around this eventually). |
I'm hoping we never need to swap browsing sessions or navigables, since it's the browsing session that'll orchestrate the joint session history. If we want to provide script boundaries to session history, algorithms can look for browsing context changes in the session history.
I thought that navigating back would either recreate the document in its original BC, or pull the document from bfcache, which would still be using it original BC.
Anything I can read in terms of reasoning there? That seems surprising to me as a developer. I'm hoping that history session items can have an associated BC, so whenever they're navigated to they use the BC they had previously. Also, when a browser is closed and restarted, pages that previously shared a BC (and group) would continue to share a BC & group.
Yeah sorry, I need to tighten my terminology there. I do mean "create a new top level browsing context", which also creates a new group. Are there cases where the BCG group isn't just bookkeeping? Can a group be defined as "A top level browsing context and all its auxiliary browsing contexts".
Cool, then I'll keep them associated with browsing contexts. |
|
I don't know if we'll need a paused browsing context, since they don't seem to execute anything when none of their documents are active. But it's almost certainly more complicated than it is in my head right now.
I'd like to allow for bfcache as much as possible in the spec. It's an optimisation, and optional, so it doesn't matter if browsers don't do it in particular situations.
Aha! Of course. That's the bit I hadn't realised. |
Wouldn't making the document inactive require some kind of state on the browsing context? And we'd have to define what various APIs would return when the browsing context is in such a state, e.g., |
The difference between Test 2 (URL-bar-nav-initiated BCG swap) and Test 3 (COOP-initiated BCG swap) in Chrome is an implementation quirk that @carlscabgro is currently trying to remove - we want the COOP behavior for all BCG swaps, I think.
Yeah whether or not to restore previous connections will be interesting. Previously we gravitated towards restoring, but thinking about it again, it's definitely simpler spec-wise and implementation-wise to not restore the connection. The downside is that it will be an observable behavior difference when bfcache is used vs not. On whether not restoring will break stuff or not, I'm not sure what's the current behavior in Chrome/Firefox actually.. maybe @jakearchibald can test it? But I think we're pretty conservative with BCG swaps in Chrome anyways (only on browser-initiated navigations, or due to COOP, or when there are no openers/openee) so it sounds OK-ish from my PoV. |
We already handle documents that aren't displayed (bfcache), and we already handle history entries with missing documents (discarded). I think I'm missing something. |
fwiw, I don't think it'll be bad spec-wise. A history entry will have a browsing context. This data lets us know when session history crosses a BCG, but it also lets us restore a document into a particular browsing context. Going through and swapping these contexts would be more spec text, not less. |
The current model assumes there's a single top-level browsing context that's 1:1 with history. So a bfcache document is inactive because it's not the active document of that browsing context. However, if history spans multiple top-level browsing contexts (as is the case with BCG swaps) and you want to retain those browsing contexts and not discard them and recreate them you can have multiple documents that are the active document, each being assigned to one of those retained browsing contexts. |
@annevk ahh I see. Yeah, that's one of the things I plan to change. Session history entries will move from the browsing context to the 'navigable', and it's the document in the current session history entry of the navigable that's active. The browsing session will pass a step number to all its descendant navigables, which they'll use to decide which session history entry should be current.
|
Still though, the current APIs assume that if a top-level browsing context exists, it's "active". If you have tab 1 and 2 and 1 has a reference to 2 and then 2 does a BCG swap, should 2 appear closed to 1? Will 1 expect that something that is closed can become unclosed later? If it does not appear closed, 1 will expect it to be active, which it's not. Implementation-wise there would be quite a few challenges making that work as I understand it, at least in Firefox. |
Yeah, it isn't interoperable right now. From test 3, it looks like Firefox does keep the document alive and accessible via the reference. I'm not sure what the right behaviour is there, although Firefox's current behaviour seems to make more sense than Chrome's, especially as Chrome's is against the IDL. |
Just happened across this behaviour which broke my understanding of the world: const wait = (ms) => new Promise(r => setTimeout(r, ms));
const iframe = document.createElement('iframe');
iframe.src = new URL('?frame', location.href);
document.body.append(iframe);
iframe.onload = async () => {
await wait(500);
iframe.contentWindow.location.hash = '#foo';
await wait(500);
location.hash = '#bar';
await wait(500);
history.go(-2);
await wait(500);
console.log({
mainHash: location.hash,
iframeHash: iframe.contentWindow.location.hash,
});
}; I expected the above to navigate the main page to '/' (the start url), and the iframe to '/?frame', but the iframe doesn't navigate. I guess the history traversal of the iframe is cancelled by the history traversal of the parent, but it seems totally unintuitive to me. @domenic is this a behaviour you want to avoid or preserve with |
Looking at the Gecko implementation it seems that if a new entry would be loaded to a browsing context A, none of its children would get an entry loaded. If I read the blame correctly, Gecko has had that behavior at least from 2002. |
@smaug---- I guess by 'new entry' you mean 'different entry'? Because in the above case it's a change of entry but it isn't a new one. |
yes, different entry, not a clone of the one currently loaded entry. Basically an entry which would trigger something to be loaded. |
@smaug---- any idea why browsers have this behaviour? I wonder if it's a bug. There's no point in navigating the iframe if it's about to be destroyed (as in, the parent is being traversed to an entry with a different document), but in this case it's being navigated to an entry with the same document. |
If I read the blame correctly, Gecko has had that behavior at least from year 2002. |
So @kjmcnee dug into Chrome and found this reference. So yeah, looks like a bug, and might even be coincidence that Firefox has the same bug. |
I don't want to change the underlying session history entry tracking behavior with appHistory, apart from the related effort of fixing foundational interop issues and then having that get reflected up into appHistory. That is, the plan is for appHistory to just be a "view" onto same-frame contiguous same-origin history entries, modulo the issues you've pointed out in WICG/navigation-api#29. Since it sounds like this is interoperably weird, and perhaps even something high-profile websites depend on, I don't think we'd do much interop bug fixing here, so I don't think app history would change things... I might not be understanding the connection you're making though. Feel free to chat me up in IRC or open an app-history repo issue. |
@domenic ta! Although, it seems like this behaviour is a bug & not spec'd anyway. I assumed it was a spec thing at first. |
I created a 10 min high level view of what I'm trying to achieve in the first pass of these spec changes https://www.youtube.com/watch?v=nZb0U3rFQXw |
Something that came up in #6809 (comment): The Some of this depends on whether sandboxing is stored along with the history entry or not. |
https://bugs.webkit.org/show_bug.cgi?id=270249 rdar://122506395 Reviewed by Chris Dumez. When a parent window uses window.open to create a child window, and the child window isn't closed, currently that causes all future navigations in the parent window to remain in the same process. In the case of a cross-origin manual navigation (e.g. navigation via the address bar), it should be safe to allow a process swap, as per the discussion here: whatwg/html#5767 (comment) Note that Safari already almost always already has this behavior even though we don't do this at the engine level, since most location bar navigations occur in a new WKWebView. The new WKWebView has no opener link to the openee and loads in a different process. So there shouldn't be any new compatibility issues with doing this at the engine level as well. See also 272321@main, where we did this for the opposite case (allowing a window with an opener to process swap on a cross-origin manual navigation). * Source/WebKit/UIProcess/WebProcessPool.cpp: (WebKit::WebProcessPool::processForNavigationInternal): * Tools/TestWebKitAPI/Tests/WebKitCocoa/ProcessSwapOnNavigation.mm: Canonical link: https://commits.webkit.org/275565@main
I'd like to update session history traversal to cater for multiple browsing contexts, and specify features that are somewhat shared by all browsers but missing from the spec. Unfortunately no browser behaves exactly the same, so I've tried to pick the common parts, then a couple of outlying behaviours that seem 'right'. I'd like to get some rough agreement on this before proceeding.
When storing/reconnecting nested session history:
name
should be used when reconnecting nested session history from one document to another (reloaded) document. Chrome/Safari currently do this.DOMContentLoaded
. Safari sort-of does this, but it will go beyondDOMContentLoaded
, but things get pretty unreliable after that point. It seems to make sense to have a clear 'deadline' for session history restoration, after which it can be discarded.My hope is this would resolve (at least large parts of) the following:
Implementers and spec folks: Are you happy with the above behaviour becoming 'the way'?
Current browser behaviour
I poked the nightlies/technical previews of Chrome, Safari, and Firefox using this test page to try and figure out how session history worked. Every browser behaves differently, so in order to specify a behaviour we need to pick the best from each browser + the current spec and compromise.
Session history and inner browsing contexts
All browsers seem to agree that session history for a 'tab' includes navigations applied to frames. The back/forward buttons treat this as a flat list that can be traversed. The spec kinda defines this with the joint session history, which is a 'getter' on the top level browsing context, which combines the history of itself and all child browsing contexts, in chronological order.
However, the way this actually works differs between Chrome, Safari, and Firefox, especially after some recent Chrome changes.
Chrome
If a history item targets a browsing context that no-longer exists (eg an iframe removed from the DOM), Chrome will retain the items in session history, but they'll be a no-op when activated.
This is also the case for iframes that are 'moved' around the DOM, since moving is remove + add, and remove discards the iframe's browsing context – Chrome does not maintain a link between the old & new browsing contexts.
Safari
I haven't fully understood Safari's behaviour yet. It seems like every item in session history has some kind of “expected browsing contexts” map, and if the reality doesn't match that map, it reloads the page from the memory cache (unless the page was served with no-store, in which case it performs a full reload) and attempts to reapply iframe navigation state in the new page.
For example:
Safari will reload the page, and set iframe 1 to 1.html. However:
Again, Safari will reload the page, which is what makes me think its logic is broader than browsing contexts involved in a given entry of session history. However:
In this case Safari will navigate the new iframe 'back' to 2.html without reloading the page. This only works if the iframe is named. This also means session history for a named iframe appears to be preserved when it's 'moved' around the DOM, even though moving an iframe causes the inner browsing context to be discarded and a new one created.
Firefox
When an inner browsing context is discarded (removed, or moved), its session history items are also discarded.
history.length
is immediately updated, but some other parts of browser UI (such as the drop down on the back button) lag behind - clicking on these entries does nothing, you remain on the same session history item.Firefox's behaviour seems best to me, and closely matches the current spec.
Navigating a context that already has 'future' history items
All browsers seem to discard all future history items in the joint session history, whereas the spec (2.otherwise.1) only removes from the current context's browsing history.
Current browser behaviour seems best here.
Changing top level browsing context on navigation
More recently, browsers will sometimes change top level browsing context as part of a navigation, which breaks session history as defined by the spec, since a navigation that changes top-level browsing context should (as defined by the spec) lose all session history, but that isn't the behaviour we want.
There's a placeholder in the spec to deal with this.
Going 'back' to pages with inner browsing contexts
All browsers seem to agree that 'deep' session history is kinda retained despite navigating the parent. For instance, if you:
…browsers will attempt to put the iframe back into its final state in terms of session history, and pressing back again will navigate it to its previous session state.
This is especially easy in browsers that can restore the previous page from the bfcache, as all the parts are still 'live'. However, the bfcache is an optimisation, and may not be used due to particular page conditions, or may be dropped due to resource constraints.
Without bfcache, browsers will still try to restore the final navigation state by ignoring the src specified on the frame, and instead using their final url from session history.
This behaviour appears to be missing from the spec. Since the joint session history is a getter, the items which reference child browsing contexts would be lost when the parent Document is discarded. Retaining session history seems important here, so the spec needs to change. However, browser behaviour isn't consistent.
The following results are based on no-bfcache, which was simulated by serving pages with Cache-Control: no-store, and adding an unload event listener to the window.
Chrome
When you navigate back to a page, Chrome will 'reconnect' iframes with session histories based on their name attribute, falling back to their connection order.
This only applies to iframes created by the parser. Any iframes created with JS (even before DOM-ready) are not reconnected with session history.
If session history items cannot be associated with particular frames (either the named iframe is missing, the parser created fewer this time, or the iframe was created with JS), the number of items in session history is unchanged, but those session items become no-ops.
Safari
Similar to Chrome, Safari will 'reconnect' iframes with session histories based on their name attribute, falling back to their connection order.
However, this includes iframes created with JS. Even if the iframe is created after DOM-ready. For instance:
Although Safari can't find iframe-1 to give it its final navigation state when the top level page is reloaded, it can 'reconnect' with it when going back through the previous session states.
Safari's “reload if browsing contexts don't look as expected” behaviour, as described earlier, also applies here. In the case of JS-created iframes, this can lead to every back-button press reloading the page but not being able to find the iframe it wants to navigate.
Firefox
Firefox will 'reconnect' iframes with session histories based on their connection order. It doesn't take the iframe's name into consideration.
Like Chrome, this only applies to iframes created by the parser. Any iframes created with JS (even before DOM-ready) are not reconnected with session history.
If session history items cannot be associated with particular frames (either the named iframe is missing, the parser created fewer this time, or the iframe was created with JS), those session history items are discarded, and
history.length
is immediately updated.Specifying the history as a 'timeline'
In the current spec, the history timeline is derived from independent session history 'current entries', where the single history timeline has to be enforced through algorithms. I'd like to flip this around so the browsing session is in charge of the current history position.
I've made a little 10 minute presentation where I go through the spec changes at a high level https://www.youtube.com/watch?v=nZb0U3rFQXw.
You can think of session history as a timeline:
(ignore the cell background colours, they're GitHub's default styles)
This example has 5 steps of session history:
In this model, each session history item is given a step number. Some history items will share a step number – in the example above there are three history items with step number 1.
The browsing session will have a 'current step', representing the current point in the timeline. The intended 'current' session history item for all navigables in a browsing session can be derived from their session history and the browsing session's current step.
For example, if the current step is 2:
It's easy to derive the current history item of the top level and all the child navigables.
Then, if the second iframe is removed:
The iframe and its session history items are simply removed. Rather than try and remove step 2 at this point, I think it's better to handle empty steps at navigation time. For example, to go 'back':
Which would be 2 in this case.
Which would take step down to 1.
Which would set the current step to 0.
If, instead, the iframe was navigated to 'hello.html':
Which would be 2 in this case.
This removes the bar.html and 3.html history entries.
Resulting in:
If a new navigable is added to the page, its initial session history step will be the same as the step of the parent navigable's current session history item. Which would be 1 in this case:
I think this would also solve a bug in the current entry of the joint session history, which would treat the session item in the new iframe as the current entry, whereas in this model the current history item would be the parent-most session history item with the current step (decrementing as necessary), which would be
hello.html
.To get the 'length' of the overall session history, take all the session history items and return the number of unique steps.
Implementers and spec folks: Would you be happy if session history was spec'd in this way?
Spec changes
I haven't thought enough about this, so there will almost certainly be changes as I work through stuff, but here's some of my notes:
A browsing session is a top-level navigable that also has:
There's never a history item with step -1. The step will be incremented when the first history item is created.
A navigable represents something that can be navigated. Things which currently contain browsing contexts for navigation purposes will use a navigable instead, and may be null in cases where browsing contexts can be null, such as a disconnected iframe. A navigable has:
A session history entry has:
Document
object (although "other information" makes me sad).Document
.Yeah, I'm unsure about making a reference weak conditionally like this. I'll look for a better way to do this. The idea is that browsing contexts can go away when they don't have documents associated with them.
Document
, a restorable session history, or null. This is the new home for theDocument
object currently on the session history entry.A restorable session history allows the browser to 'reconnect' child contexts with their session history when a document is recreated for a session history item. A restorable children's session history has:
When an iframe is connected for the first time before document parsing is complete, it takes its URL from the restorable session history if possible, and falls back to its src. A weak reference to it is added to a list of navigables that may be serialised as part of restorable session history when the parent
Document
is discarded. TODO: I'm not yet sure of the exact timing of this, eg when is an iframe associated with its name in terms of history restoration?In my model, an iframe's current browsing context is only accessible through its session history. @domenic doesn't think this will work, but I'd like to see how far I get with this 'pure' model before unravelling it a bit.
Before I spend more time figuring this out, am I at least heading in the right direction?
The text was updated successfully, but these errors were encountered: