There probably isn't a web developer that hasn't heard this, "When I hit the back button, I get a WebPage Expired message".
Let's see if we can recreate this with a simple VisualForce page and controller example:
Problem:
<apex:page controller="DemoController" >
<apex:outputText value="{!message}"/>
<apex:form >
<apex:outputLabel value="Type a message:" for="messageField" />
<apex:inputText id="messageField" value="{!message}" />
<apex:commandButton action="{!post}" value="Save" />
</apex:form>
</apex:page>
public with sharing class DemoController {
public String message{ get; set; }
public PageReference post(){
/* do something here */
return null;
}
}
This creates a page that looks something like this:

When you type a message and click the "Save" button it executes the post action in the DemoController. This controller doesn't do much at all, but typically you might update a record, do an upsert, etc. If we do this a couple of times and then hit the back button we will be greeted with a message that looks like one of these:
Repost form data message in IE8, Safari, Chrome, and Firefox:




Now really IE8 is the only one that incorrectly states that the WebPage has expired. Safari, Chrome and Firefox get it right and warn us about resubmitting (POSTing) our form data again. If we fire up Fiddler we can watch the HTTP(S) traffic that is generated.
When we click the "Save" button we can see the following Request and Response messages:
(Note: I edited the traffic messages below to make them more readable and remove unnecessary cruft.)
Request
POST https://c.na7.visual.force.com/apex/DemoPage HTTP/1.1 Host: c.na7.visual.force.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Referer: https://c.na7.visual.force.com/apex/DemoPage Cookie: sid=00DA0000000akOY!ARkAQPm5hbsddMNqU2kEA4MXGelClG3Z6I9x3VEkWP4rr.5Erw92QVopCcwCVRepx_JDnaeOxfGDpR9XquO.__RaszYIO2Jk; sid_Client=0000000Kg7b0000000akOY; clientSrc=67.189.32.220; inst=APPA Content-Type: application/x-www-form-urlencoded Content-Length: 4829 ...
Response
HTTP/1.1 200 OK Server: Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private X-Powered-By: Salesforce.com ApexPages P3P: CP="CUR OTR STA" Pragma: no-cache Expires: Fri, 03 Sep 2010 23:19:07 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 29302 Date: Fri, 03 Sep 2010 23:19:07 GMT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> ... <h1>Post-Redirect-Get Demo</h1> <div>Test </div> <form id="j_id0:j_id4" name="j_id0:j_id4" method="post" action="https://c.na7.visual.force.com/apex/DemoPage" enctype="application/x-www-form-urlencoded"> <label for="j_id0:j_id4:messageField">Type a message:</label> <input id="j_id0:j_id4:messageField" type="text" name="j_id0:j_id4:messageField" value="Test" /> <input type="submit" name="j_id0:j_id4:j_id6" value="Save" class="btn" /> </form> ... </html>
Notice that we made a single request (POST) and got back a web page in the response. If at this point we hit the back button, we will be asking the browser to re-POST the form data. Our web browser will respond to this request with one of the previous warnings.
Solution:
So how do we deal with this problem? One approach is to use the Post-Redirect-Get pattern. Post-Redirect-Get (PRG) is a web development pattern that attempts to solve the problem with a bit of indirection.I think this article boils it down nicely:
"PRG pattern splits one request into two. Instead of returning a result page immediately in response to a POST request, server responds with redirect to result page. Browser loads the result page as if it were an separate resource. After all, there are two different tasks to be done. First is to POST input data to the server. Second is to GET output to the client."
So let's modify our controller from the previous example to implement the PRG pattern:
public with sharing class DemoController {
public String message{ get; set; }
public PageReference post(){
/* do something */
PageReference p = new PageReference('/apex/DemoPage');
p.setRedirect(true);
return p;
}
}
Notice that our new post method is calling setRedirect(true) on the PageReference.
The SalesForce Visualforce Developers guide give us this handy little tip:
You can use the setRedirect attribute on a pageReference to control whether a postback or get request is executed. If setRedirect is set to true, a get request is executed. Setting it to false does not ignore the restriction that a postback request will be executed if and only if the target uses the same controller and a proper subset of extensions. If setRedirect is set to false, and the target does not meet those requirements, a get request will be made.So setting setRedirect to true returns a PageReference that will perform a GET request. This seems to fit the PRG pattern. We make a POST request that redirects to a GET.
Let's save our code, retest it, and see what Fiddler has to say:
Request:
POST https://c.na7.visual.force.com/apex/DemoPage HTTP/1.1 Host: c.na7.visual.force.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Referer: https://c.na7.visual.force.com/apex/DemoPage?save_new=1&sfdc.override=1 Cookie: sid=00DA0000000akOY!ARkAQPm5hbsddMNqU2kEA4MXGelClG3Z6I9x3VEkWP4rr.5Erw92QVopCcwCVRepx_JDnaeOxfGDpR9XquO.__RaszYIO2Jk; sid_Client=0000000Kg7b0000000akOY; clientSrc=67.189.32.220; inst=APPA Content-Type: application/x-www-form-urlencoded Content-Length: 4969 ...
Response:
HTTP/1.1 200 OK
Server:
Cache-Control: private
X-Powered-By: Salesforce.com ApexPages
P3P: CP="CUR OTR STA"
Content-Type: text/html; charset=UTF-8
Content-Length: 401
Date: Fri, 03 Sep 2010 23:30:42 GMT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
<script>
if (window.location.replace){
window.location.replace('https://c.na7.visual.force.com/apex/DemoPage');
} else {;
window.location.href ='https://c.na7.visual.force.com/apex/DemoPage';
}
</script>
</head>
</html>
Side Note:
What is interesting here is that SF redirects us with javascript. If you read the setRedirect documentation for the PageReference object, it says that "If set to true, a redirect is performed through a client side redirect." This Wikipedia article discusses URL redirection in all of its forms.
Request:
GET https://c.na7.visual.force.com/apex/DemoPage HTTP/1.1 Host: c.na7.visual.force.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Referer: https://c.na7.visual.force.com/apex/DemoPage Cookie: sid=00DA0000000akOY!ARkAQPm5hbsddMNqU2kEA4MXGelClG3Z6I9x3VEkWP4rr.5Erw92QVopCcwCVRepx_JDnaeOxfGDpR9XquO.__RaszYIO2Jk; sid_Client=0000000Kg7b0000000akOY; clientSrc=67.189.32.220; inst=APPA
Response:
HTTP/1.1 200 OK Server: Cache-Control: no-cache, must-revalidate, max-age=0, no-store, private X-Powered-By: Salesforce.com ApexPages P3P: CP="CUR OTR STA" Pragma: no-cache Expires: Fri, 03 Sep 2010 23:30:42 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 29202 Date: Fri, 03 Sep 2010 23:30:42 GMT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> ... <h1>Post-Redirect-Get Demo</h1> <div></div> <form id="j_id0:j_id4" name="j_id0:j_id4" method="post" action="https://c.na7.visual.force.com/apex/DemoPage" enctype="application/x-www-form-urlencoded"> <label for="j_id0:j_id4:messageField">Type a message:</label> <input id="j_id0:j_id4:messageField" type="text" name="j_id0:j_id4:messageField" /> <input type="submit" name="j_id0:j_id4:j_id6" value="Save" class="btn" /> </form> ... </html>
So that was a lot to digest. But essentially setRedirect(true) does solve our problem.
Conclusion:
PRG is not a magic bullet, but I hope that this article gives you some insight and tools for dealing with this type of problem. Happy coding!For more information:
http://en.wikipedia.org/wiki/Post/Redirect/Gethttp://www.theserverside.com/news/1365146/Redirect-After-Post







Hi Matthew, Thanks for the article. How would you test for the proper redirect in a Test method? Here is my example.
ReplyDeletepublic ApexPages.StandardController Scontroller {get; set;}
<...constructor method...>
<...save method...>
PageReference pv = Scontroller.view();
if (ApexPages.hasMessages()) {
pv.SetRedirect(false);
return null;
} else {
pv.SetRedirect(true);
return pv;
}
Sorry for the late response. I think what you are asking is, "what do you do if a post fails?" Like in the case where a validation fails. This is a sticky wicket. I think the correct way to deal with it comes down to a combination of client-side validation and session state. So if we use client-side validation, we can hopefully avoid the errant postback. Failing that, we store the users submissions in session and redirect them back to the form again. The form pulls the session state out and loads the page with error messages. Not always ideal, but prevents back button issues.
ReplyDelete