Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

post-redirect-get (PRG) re-inserts same page into history in Webkit browsers

Let's say I'm on /page?id=1

Then I navigate to /page?id=2

And I make a change on that page, which implements a post and then redirects back to /page?id=2

In Firefox, I can hit the back button once and return to /page?id=1, but in Chrome and Safari on iPhone, I have to hit the back button twice because /page?id=2 is in the browser history twice. (And if I made multiple posts from id=2, I'd have to hit the back button that many times to finally return to id=1.)

In some ways, this seems like normal browser behavior, as each GET is simply pushed into the history, but since the URL is identical to the previous entry, this results in a poor user experience, which typically seems to be avoided by other web applications... and is naturally avoided in Firefox. Is this an unavoidable bug in Webkit browsers, or can I implement the PRG in a different way to avoid this?

btw- the behavior appears to be the same redirecting with 302 or 303.

UPDATE: I've mocked up some sample code... don't know if there's a platform like jsfiddle where I could upload this for you to see in action:

form.php:

id=<?=$_REQUEST['id']?>
<form action="submit.php" method="post">
<input type="hidden" name="id" value="<?=$_REQUEST['id']?>">
<input type="submit" value="submit">
</form>

submit.php:

<?php
header("Location: form.php?id=" . $_REQUEST['id']);
die($_REQUEST['id']);
?>

If I start on form.php?id=4 (just to put it in the browser history) and then go to form.php?id=5 and then hit submit (as though to execute a database change), in Firefox I get one entry in the history for each; in Chrome I get one entry for id=4 and then two entries for id=5. Why the difference in behavior? I think Firefox's behavior is better, since hitting back twice to get away from id=5 is counter-intuitive to the user.

like image 788
dlo Avatar asked Jul 14 '16 21:07

dlo


2 Answers

Though it's not an explanation on what happens, I have a way around it that I use in all of my applications. First some code:

form.php would look like this:

id=<?=$_REQUEST['id']?>
<form action="submit.php" target="iframe" method="post">
    <input type="hidden" name="id" value="<?=$_REQUEST['id']?>">
    <input type="submit" value="submit">
</form>

submit.php like this:

<?php
    header("Location: form.php?id=" . $_REQUEST['id']);
    die($_REQUEST['id']);
?
<script>
    window.parent.location.reload();
</script>

It's your documents with some extra stuff in the <form> tag and a brand new <script> tag.

I would also have an iframe somewhere in the document like this:

<iframe name="iframe"></iframe>

So to explain. Instead of navigating to a new site and back again every time you need to make a change, you simply load the submit.php in an iframe in your existing document. Hence the target="iframe" part.

Then when the iframe has loaded, meaning the changes have been made, you reload the original page to reflect these changes, hence the window.parent.location.reload(); part. Since the page is just reloading, it won't put a second entry in your history.

I hope this helped you :)

like image 187
atjn Avatar answered Oct 11 '22 22:10

atjn


We also experienced the same problem and even after researching for days I found no "easy" solution. The closest thing I found was this Webkit Bugzilla ticket, which by the looks of it doesn't seem to be very high priority. As you mentioned, IE and Firefox behave just fine.

Since we have our own back button in the application, we were able to solve the problem by using the session storage and checking it when the page is loaded. The TypeScript code is the following:

class DoubleHistoryWorkaround {

    // key of the attribute we store the URL we where on when clicking on the back button
    private static comingFromLabel = "comingFromURL";
    // key of the attribute of the flag denoting whether we set a valid comingFromURL
    private static comingFromFlag = "comingFromSet";

    constructor() {
        this.checkLocation();
    }

    /**
     * Checks the URL we saved in the session and goes a further step back
     * in the history if the first back button brought us to the same page again.
     */
    private checkLocation() {
        let doubleEntry : boolean;
        // have we set a comingFromURL?
        let comingFromSet = window.sessionStorage.getItem(DoubleHistoryWorkaround.comingFromFlag);
        if (comingFromSet) {

            // is the set comingFromURL the same as our current page?
            let currentURL = window.location.href;
            let comingFromURL = window.sessionStorage.getItem(DoubleHistoryWorkaround.comingFromLabel);
            if (currentURL === comingFromURL) {
                // double history entry detected
                doubleEntry = true;

                // before we skip we save our location ourselves, since we might still navigate
                // to the same page again (in case of triple identical history entries)
                DoubleHistoryWorkaround.saveLocation();

                // skip this page
                history.back();
            }
        }

        // reset the location entry unless we just set it ourselves
        if (!doubleEntry) {
            this.resetLocation();
        }
    }

    /**
     * Saves the current location in the session storage.
     */
    public static saveLocation() {
        window.sessionStorage.setItem(DoubleHistoryWorkaround.comingFromFlag, "true");
        window.sessionStorage.setItem(DoubleHistoryWorkaround.comingFromLabel, window.location.href);
    }

    /**
     * Removes the set location from the session storage.
     */
    private resetLocation() {
        window.sessionStorage.setItem(DoubleHistoryWorkaround.comingFromFlag, "false");
        window.sessionStorage.setItem(DoubleHistoryWorkaround.comingFromLabel, "");
    }
}

We call DoubleHistoryWorkaround.saveLocation() when we click our application's back button, setting the session entries that are checked by checkLocation().

like image 44
ilydlci Avatar answered Oct 11 '22 20:10

ilydlci