-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Refactor history state management to use history.pushState() within 500ms of last user interaction. #1087
Conversation
…00ms of last user interaction.
…ory cache (homepage "/" becomes "")
@croxton do you have a reference for this behavior for safari? |
I couldn't track down any specific details of Safari's current history API implementation, but I did find this thread where various interventions to prevent pushState() abuse were discussed, and the figure of 500ms was first mentioned: My testing strongly suggests Safari has indeed implemented the 500ms intervention, or something that approximates it, in the latest major version of iOS (I did not test older versions). Specifically, a call to pushState() after 500ms since user interaction creates a history entry (history.length is incremented) but that is somehow flagged to be skipped on browser back button press. A call to history.back() triggered by a click DOES work, confirming the entry exists. Clicking the forward button also skips the history state entry. |
"Per WICG/interventions#21, some browsers have implemented a heuristic where pressing the back button skips certain entries in the joint session history. Generally, these are entries where there is no user interaction. The intent is to avoid "back trapping", e.g. if you arrive on a malicious site which does history.pushState() 10 times, this makes it hard to escape the site by pressing the back button.": |
@croxton reading through the docs here and there, would the following logic make sense: On every htmx request, regardless if it pushes state or not, issue an htmx.replaceState() that looks at the current history.state, doc title and path? Is my understand that, once we have this entry in place, we can mutate later, even after 500ms? And if we don't then nothing has really changed? Does that sound right to you? |
You need to do a So, for every htmx request where we know the URL will change I think we need to issue a |
DAMMIT. OK, lemme think some more. (Sorry, grug brained) |
…everted to original htmx behaviour). Only change URL on pushState if we know for sure what it will be (hx-push-url or hx-boost).
…ecify that it should not be cached in localStorage (useful for sensitive or session-specific data).
Couple of updates, which came from trying out this change on two more projects. First, the point at which the current page is cached I changed back to on response received (i.e. reverted to how it always has worked in htmx). I was able to get that working without the currentPage url being mangled by the earlier pushState. That means except for pushState and the replaceState call points, things work much as before. I also realised that data could enter localStorage that you may not want in there, when the current page is saved to cache. I added an option to use |
…tory and cache key normalisation).
The main reason this change wasn't passing the <meta name="htmx-config" content='{"historyEnabled":false,"defaultSettleDelay":0}'> I'm not sure why/if it worked before but for stuff to be written to the history cache that value would need to be true (the default), unless I'm misunderstanding what that config option is meant to do? Anyway, the relevant automated tests now all pass if history is enabled. |
I fixed |
Hi @croxton, given our conversation I'm still interested in the ignore history and normalize path parts of this change, and I'd like to keep your work around in a branch in case this behavior rears its ugly head in the future. I'm planning on cutting the 1.8.3 release at the end of the week. No worries if your stuff doesn't make it in, but wanted to let you know in case you want it in the relase. Thanks again for all your amazing work on this issue, sorry about the gray hairs! |
@croxton are you ok w/ me closing this for now? looking forward to what you do w/ htmx 2.0 history! |
@1cg Sure please go ahead. I have some ideas which I need to ruminate on a little, and may be a little out of scope (not sure yet) but we'll see. Excited to see where this is going! |
Fix for #1076.
Safari on iOS skips history state entries when using navigating backwards/forwards IF they were created using
history.pushState()
more than approx 500ms after the last user interaction (link click, select menu change etc). This makes back/forwards buttons behave unpredictably with htmx applications on all current iPhones and iPads, given that network conditions and application response times may vary.Currently htmx executes a
history.pushState()
once a response has been received. This PR moves the pushState to before the response has been received, and then updates the final intended URL with ahistory.replaceState()
(since replaceState doesn't create a history entry, this isn't subject to the user interaction limit). This change has a knock-on effect on cache management, which is also addressed. There may be some other implications around error handling and redirects initiated by the response that will require review (since we can no longer assume that pushState is possible after waiting for a response to be received).Some automated tests relating to
hx-push-url
are failing, and any help with that would be much appreciated. Manual tests seem to work as expected.