Bug 145953 - pushState and navigation sequence causes document to be requested when it shouldn't
Summary: pushState and navigation sequence causes document to be requested when it sho...
Status: NEW
Alias: None
Product: WebKit
Classification: Unclassified
Component: History (show other bugs)
Version: 528+ (Nightly build)
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Nobody
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2015-06-12 19:09 PDT by Iraê
Modified: 2016-01-11 21:31 PST (History)
4 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Iraê 2015-06-12 19:09:51 PDT
When using `window.history.pushState` on Mobile Safari, iOS 7, iOS 8 or iOS 9, and doing the following sequence, webkit is starting a request to the server and rebuilding the document when it shouldn't.


Steps to reproduce (just for the record, but contained in the example below):
1. Go to page A, that implements history.pushState and history.replaceState.
2. Page A uses replaceState to store current page, creating PageA-state1
3. Navigate internally using a link, that uses pushState, does Ajax request then uses replaceState to create PageA-state2
4. Navigate to a different document. After a while, or after "minimizing Safari", press back

Expected: PageA context should be restored, pop state event to fire, and restore to PageA-state2
What's happening: URL that was pushed during step 3 is requested and PageC is constructed from scratch.

I talked to Brady Eidson on WWDC 2015 Lab Sessions and we were able to reproduce. He created a minimal version but the exact conditions were not super precise.
I am submitting additional details here, and I uploaded an example case here:

https://github.com/irae/pushstate-bugs

During the Safari Labs at WWDC Brady suspected it was a process communication issue, but I believe it's something related to serialization to disk of the history states.
If I load the example on testing devices (which don't have anything special, not even apps installed), the but is not possible to reproduce right away. On the other hand, if you put Safari to background and come back, it triggers the error 100% of the times.

Another way of reproducing the error is to hold your finger on the back button and selecting the entry from the list.
For good measure I tested also Firefox, Chrome and Safari desktop. Chrome has a similar bug, which I will be submitting, but Safari on desktop and Firefox never hit the server for the URL that is not expected to be requested.
Comment 1 Brady Eidson 2015-06-12 20:37:55 PDT
After more time of looking into this after the lab, and thinking about it while *not* tired and busy, I think this behavior is correct.

At least that's what I concluded with the concocted example we were considering in the lab.

I don't have time to look closely at this test case ATM, but will either this evening or tomorrow.
Comment 2 Iraê 2015-08-07 16:50:45 PDT
I still think there is something wrong here. I see diferences when binding 'onpopstate' before and after 'onLoad' have fired. How binding the pop state event later cause the page to not be re-requested?
Comment 3 Stefan Arentz 2016-01-08 19:49:16 PST
This has been one of the top crashers for Firefox for iOS.

We narrowed our crash down to our usage of history.pushState() to simulate history restore for tabs. (Which is not an awesome hack and a workaround for the lack of a mutable BackForwardList)

Here is a simple POC that makes both Safari and Firefox for iOS crash:

  https://people.mozilla.org/~sarentz/t/boom.html

Tested on an iPad Mini running 9.1. Has been reported on 9.2 too. Including high end devices like 6s. May depend on memory usage in general. It is more reliable to reproduce if you open a lot of apps first.

This causes a memory pressure on the device that results in the OS killing us.

(People have been reporting this bug to us as 'my whole device reboots' because they see the white apple on black screen appear, but now we know that due to the high memory pressure Springboard is also simply killed, which looks like a reboot)
Comment 4 Iraê 2016-01-11 18:30:33 PST
I guess this might explain why it is reproducible before onLoad fires and there is no issue after onLoad. I would guess the loading process is memory intensive.

Also would explain why in complex scenarios, like previous iterations of Yahoo Search, this was 100% reproducible, while I had trouble creating local POCs.
Comment 5 Brady Eidson 2016-01-11 21:16:53 PST
(In reply to comment #3)
> This has been one of the top crashers for Firefox for iOS.
> 
> We narrowed our crash down to our usage of history.pushState() to simulate
> history restore for tabs. (Which is not an awesome hack and a workaround for
> the lack of a mutable BackForwardList)
> 
> Here is a simple POC that makes both Safari and Firefox for iOS crash:
> 
>   https://people.mozilla.org/~sarentz/t/boom.html
> 
> Tested on an iPad Mini running 9.1. Has been reported on 9.2 too. Including
> high end devices like 6s. May depend on memory usage in general. It is more
> reliable to reproduce if you open a lot of apps first.
> 
> This causes a memory pressure on the device that results in the OS killing
> us.
> 
> (People have been reporting this bug to us as 'my whole device reboots'
> because they see the white apple on black screen appear, but now we know
> that due to the high memory pressure Springboard is also simply killed,
> which looks like a reboot)

This bug report is about a server request that is unexpected.

You are describing a crash, which is a completely different issue from what is being reported.

Please file a new bug with steps to reproduce and, ideally, a crashlog.
Comment 6 Brady Eidson 2016-01-11 21:31:05 PST
Iraé, here's why I don't think this is a bug.

> 1. Go to page A, that implements history.pushState and history.replaceState.

Let's call URL-A "http://foo.com/index.html"

The WebView has only ever loaded a single document - DOC-A
The back/forward list has one entry:
URL-A (DOC-A)

> 2. Page A uses replaceState to store current page, creating PageA-state1

The WebView has still only ever loaded a single document - DOC-A
The back/forward list now has one entry:
URL-A (DOC-A, with state object A1)

> 3. Navigate internally using a link, that uses pushState...

Let's say the pushState is to URL-B "http://foo.com/bar.html"

The WebView has still only ever loaded a single document - DOC-A
The back/forward list now has two entries:
URL-A (DOC-A, with state object A1), URL-B (DOC-A, with state object B1)

>...does Ajax request then uses replaceState to create PageA-state2

The WebView has still only ever loaded a single document - DOC-A
The back/forward list now has two entries:
URL-A (DOC-A, with state object A1), URL-B (DOC-A, with state object B2)

> 4. Navigate to a different document.

Let's call the navigation to URL-C "http://webkit.org/"

The WebView has now navigated to TWO different documents, DOC-A and DOC-C
The back/forward list now has three entries:
URL-A (DOC-A, with state object A1), URL-B (DOC-A, with state object B2), URL-C (DOC-C)

At this point in time, DOC-A might no longer exist. If DOC-A was eligible to go into the page cache, then it still exists in a suspended state. But if something about DOC-A rendered it ineligible, it is gone. Forever.

> After a while, or after "minimizing Safari"...

"After awhile" might mean, say, 3 hours. At this point, even if DOC-A were still suspended in the page cache, we would've invalidated it. It is too old for us to confidently resume.

Or, if you minimize Safari and then go launch some other apps that use memory on your device, WebKit would get a low memory warning and purge DOC-A, since it is not obviously usable anymore.

> ...press back.

You are viewing URL-C (DOC-C), and you press back. If DOC-A still exists, the "back" command is telling WebKit "Resume DOC-A at URL-B, popping state object B2"

But if anything has happened to purge DOC-A, then the "back" command is telling WebKit "Load URL-B, then pop state object B2"

Your server is likely to actually get pinged for URL-B, since WebKit never actually loaded a URL-B - Remember the user originally visited "http://foo.com/index.html" yet now WebKit is being asked to load "http://foo.com/bar.html", which has never actually been loaded.

This is why when people use pushState/replaceState it is important that they use real world URLs. Those URLs are not meant to be some app-internal placeholder, but rather actually URLs that might actually be hit in a network request.

Does this explanation make sense to you? Is there anything else I can help clarify?