About the Author

Chris Shiflett

Hi, I’m Chris: web craftsman, community leader, husband, father, and partner at Fictive Kin.

How to Avoid "Page Has Expired" Warnings

  • Published in PHP Magazine on 21 Oct 2004
  • Last Updated 21 Oct 2004

Welcome to the first edition of Guru Speak, a new column that I'll be bringing to you every other month right here in PHP Magazine. The topics that I'll be writing about will vary, but one recurring topic that I want to focus on is that of providing thorough answers to frequently asked questions. This is a topic that is appropriate for PHP developers of all skill levels, and while some questions are not likely to concern experienced developers, the answers will hopefully lend new insight and provide a deeper understanding to everyone.

As one of the most active contributors to the PHP general mailing list for the last few years, I have answered many questions, and it's pretty easy to determine which topics trouble developers the most. While part of the reason for the frequency with which some questions are asked is that people don't search the archives or the Web prior, there are also some questions for which there is no sufficiently complete answer available, because no one feels the need to provide one. In other cases, there are so many RTFM responses that search results become very diluted, making the real answers hard to find.

It is my sincere hope that this column can begin to address these types of problems, and I would like to thank the fine folks at PHP Magazine for agreeing to make my answers freely available on the Web. I am very interested in feedback, so please feel free to contact me anytime - perhaps you have new insight to add to one of my answers, a correction to make, a suggestion for the column, or a question of your own.

I hope you enjoy Guru Speak.

How Can I Avoid "Page Has Expired" Warnings?

One problem with organizing questions and answers, and in fact a big reason why searching the archives can be difficult for a beginner, is that the same question can be posed in so many different ways. This is especially true when the same problem arises under different circumstances, or when differences in browser behavior can cause the same error to be reported in numerous ways.

A good example of this is the warning message declaring that a page has expired. Not only are there two different causes for this warning, but the warning message itself also varies from browser to browser, even under the same circumstances.

The warning is a result of the user utilizing the browser's history mechanism to access a previously viewed page, usually by clicking the back button or refreshing. One cause of the warning is that the page has expired from the browser's cache, so it wants to inform the user that it must request the page again, in case this is unwanted.

Luckily, you have some control over what a browser caches. In fact, so long as you don't forbid it, each page the user visits is cached. There are exceptions to this, such as when the user specifically configures the browser to prevent caching, or when the user sets the maximum cache size too small. These exceptions are of little concern, because the user either does this intentionally and understands the consequences, or the user will have an adverse experience on any site and likely realize that the problem is a local one.

PHP, by default, does not forbid caching. When you request a simple PHP script, no caching headers are returned. You can test this by using the LiveHTTPHeaders extension for Firefox and requesting a script such as the following:

echo '<p>Guru Speak!</p>';

Of course, no PHP application has scripts quite this simplistic. In fact, most PHP applications use sessions, and this is where caching becomes a problem. Add session_start() to the previous example, so that it becomes the following:

echo '<p>Guru Speak!</p>';

When requesting this new script, three new HTTP headers are included in the response:

Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

These new headers eliminate caching, and this is how a stateful PHP application can so easily encounter "Page Has Expired" warnings.

What can be done? Before you change anything, it is best to consider the reasoning behind this behavior. When you use sessions, it is possible that you customize each page, and these customized pages might contain sensitive information. If an intermediary caches this sensitive information, it might be returned to the wrong user. Consider the series of events illustrated in Figure 1.

Figure 1:

Caching Can Potentially Cause Problems

This situation is unwanted, because it exposes Jack's profile to Jill. Luckily, the Cache-Control header gives us a way to distinguish between public caches (such as an intermediary) and private caches (such as the browser). The following header allows caching in private caches:

Cache-Control: private

You can use header() to set this, but a better approach is to change the session.cache_limiter PHP configuration directive, because this also eliminates the Pragma header. If you don't have access to php.ini, you can set this in your scripts with the following:

ini_set('session.cache_limiter', 'private');

By default, this is set to nocache. With it set to private, the following Cache-Control header is sent:

Cache-Control: private, max-age=10800, pre-check=10800

The max-age and pre-check directives are given in seconds, and these can be used to limit the amount of time a page is cached. The session.cache_expire PHP configuration directive, given in minutes, controls these values. The default is 180 minutes (10800 seconds).

Now that you can allow your pages to be cached, there is still one other scenario that can cause a "Page Has Expired" warning. When a page in history (including the current page) is requested with the POST request method, the browser will warn the user before requesting the page again, because a POST request might perform some action. Most browsers explain this in the warning, and Firefox's warning is displayed in Figure 2.

Figure 2:

Firefox warns the user before resending a POST request.

Internet Explorer still gives a "Page Has Expired" warning but goes on to explain the situation further:

The page you requested was created using information you submitted in a form. This page is no longer available. As a security precaution, Internet Explorer does not automatically resubmit your information for you.

As a PHP developer, you might be thinking that the request method doesn't really matter to you. You can still perform the exact same actions; the only difference is that you use $_GET instead of $_POST in your programming logic. While this is true, it violates the HTTP specification. Section 9.1.1 of RFC 2616 has the following to say:

In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered "safe". This allows user agents to represent other methods, such as POST, PUT and DELETE, in a special way, so that the user is made aware of the fact that a possibly unsafe action is being requested.

This is exactly why browsers treat responses to POST requests differently than responses to GET requests.

Luckily, there is an elegant solution to this problem. You can still use the POST request method in your forms, but you hide the target page from the browser's history mechanism. The trick is to have the target page (that processes the form) send a Location header and change the response code 302. This transparently redirects the user to another page. To illustrate this, consider three PHP scripts: form.php, process.php, and end.php:


<form action="process.php" method="POST">
<!-- Rest of Form --> 


header('Location: http://example.org/end.php');
/* Form Processing Here */



When a user submits the form, the browser requests process.php using POST, and all of the form data is included. The response it receives is a 302 response, and the Location header indicates the URL it needs to request to get the page it seeks. After making the request for end.php, a message of thanks is displayed in the browser. If the user clicks the Back button, the form is displayed; the intermediate processing script is hidden from the browser's history.

The HTTP specification requires that the Location header indicates an absolute URL (such as http://example.org/end.php). See section 14.30 of RFC 2616 for more information.


To avoid "Page Has Expired" warnings, set session.cache_limiter to private, and make sure that any form using the POST method submits to an intermediate processing page that redirects the user to a different URL.

I hope you can now avoid these unwanted warnings, and I also hope that I have helped to uncover some of the mystery surrounding them. That's all for Guru Speak. See you next time.

About this article

How to Avoid "Page Has Expired" Warnings was last updated on 21 Oct 2004. Follow me on Twitter.


1.David Bates wrote:

This is simply uber cool stuff!! I love the concept of the column. Where can I find the rest of the Guru Speak coulmns? Thanks Shiflett.

Tue, 14 Dec 2004 at 13:26:57 GMT Link

2.Chris Shiflett wrote:

Hi David,

Thanks for the kind words. Currently, this is the only free Guru Speak column - the second one was just published in PHP Magazine.

They have agreed to let me publish these columns for free once they've been in print for a few months, but the most recent ones will only be available to subscribers.

Fri, 17 Dec 2004 at 04:15:31 GMT Link

3.kenrick buchanan wrote:

So the solution doesnt really work then if you are using the same page to process your form. Is there anyway to avoid that? Im thinking no.

Sat, 08 Jan 2005 at 01:16:40 GMT Link

4.Ian wrote:

Kenrick, this is also my situation. I'm writing a shopping cart order form where all processing is done in one script. If the user finds errors in their entry on the 'confirm' page and clicks back to edit the information, they get the post error. This is what I'm trying to avoid.

Any solutions?



Wed, 09 Feb 2005 at 23:22:27 GMT Link

5.Chris Shiflett wrote:

You can do it with a single script, although I'm not a big fan of that (I prefer more modularity). The idea is exactly the same, only the three scripts are essentially the same.

You are already basically making a single script behave as two - displaying the form and processing/displaying the results. So, all you need to do is divide up the processing and displaying, and you'll redirect back to yourself after processing.

Wed, 09 Feb 2005 at 23:33:20 GMT Link

6.vincent dupont wrote:


there are 2 larger articles on this topic on : http://www.theserverside.com/news/thread.tss?thread_id=29758

Wed, 16 Feb 2005 at 23:09:55 GMT Link

7.oops wrote:

@Kenrick:Is this ok if use one page to process form

switch $op

case "form":

//display a form

case "process":


//process form

case "end"

//display the end info

Sat, 02 Apr 2005 at 08:43:03 GMT Link

8.Michael wrote:

Hi ,

Thanks for this great information. but my form submited on the same page.and when everything is ok.. it redirects to "thanku" page[using php's header('location:thanku.php') function]. but when i hit browser's back button in thanku.php page. my previous page display:"Warning:This page has expired". any solutions please ?

Tue, 21 Jun 2005 at 12:38:26 GMT Link

9.Tony wrote:

Hi, firstly I'd like to say thanks for this excellent article.

I've got a script that has a multi-part form in a single page. For example, the first page displays a field to enter a name, the second is entering security options, the third is a review and confirmation page. They are all in one script, but after they get to the preview page, hitting back then forward again shows the "page expired" error.

I'm not sure if anyone has a solution to this, but it would be much appreciated.

Mon, 04 Jul 2005 at 00:52:56 GMT Link

10.Mark wrote:


I had the same problem with "Page Expired" --- I also was unable to fix it with PHP, Meta, etc...

HOWEVER, I did come up with a "work-around" by just openning up the target script in a new (target="_blank") window.

I'm not sure this technique will work for everybody's application, but in my case it did...

Tue, 02 Aug 2005 at 14:21:29 GMT Link

11.Nathan Lenz wrote:

Interesting article, I hadn't seen this written up before.

I may have a solution for those who really need to process a form in the same script the form resides. I needed to do this because I use PEAR Quickform and Controller class to build multi-page forms. Here is a quick overview of my solution.

I instantiate the form and controller objects in a file called form.php. This form submits to form-action.php.

form-action.php gets the _POST data and puts it in a "dirty_form_name" array in the _SESSION. Then it 302 redirects back to form.php.

form.php always looks for a "dirty" array in the _SESSION, if it finds one, it knows that it has been submitted and it uses that data to process the form.

Needless to say, this is a little extra work and does affect performance. You should also be careful of sensitive data being stored in the session. You may want to write a custom session handler that encrypts the session data before writing it to disk or database or whatever.

Wed, 12 Oct 2005 at 12:58:17 GMT Link

12.bzikofski wrote:

great article !

but it describes a situation when using sessions in the form, is it possible to do that without sessions ? (i guess not, you have to hold the variable somewhere between process.php and end.php...)


Mon, 24 Oct 2005 at 12:15:40 GMT Link

13.bzikofski wrote:

btw, adding a comment takes ages.. the form processes and the page wouldnt refresh. i have to do that manually..

Mon, 24 Oct 2005 at 12:19:53 GMT Link

14.Ian Leckey wrote:


Thank you for providing such a useful and thought provoking article. The only real weapon I use against this "error" (although I wouldn't really call it an error), is providing a link back to the original page. That way instead of accessing the page from the cache, it accesses it directly from the webserver. Although I have also used the three script example before, however I avoid using it now, as I hate having large amounts files everywhere. It makes the directory trees hard to plan, especially if you have lots of pages which use the three script example. As for the headers, I usually use "Cache-Control: no-cache", however I still get the POSTDATA warning in firefox. But for what I use it for, the warning does not bother me. It is a slight inconvenience to me, but that is all. Supplying a link I can click on is fine for me (although maybe not for my users).

Thu, 27 Oct 2005 at 12:39:33 GMT Link

15.Jeanne A. Brohart wrote:

My website was recently hacked and a lot of the links show "page expired"...

So... what do you do to prevent and/or fix something like this... I'm not a programmer... so... could you explain in layman's terms... thanks.

Wed, 02 Nov 2005 at 13:26:53 GMT Link

16.Dongsu Kim wrote:

It works very well. Thank you so much!!

Fri, 04 Nov 2005 at 19:17:09 GMT Link

17.Jrf wrote:

Very informative article !

I often process a form in one same script having the display processing and thank you actions seperated using case to determine the script action.

If the processing case actions, after doing what it has to do, I normally use:


This has the effect of eliminating the warning messages when a user browses back as no $_POST data will be found.

What is your opinion of this method ?

Sat, 05 Nov 2005 at 16:42:58 GMT Link

18.bzx wrote:

it's a great article but..

when i'm at the "thank you" page, and hit REFRESH i will get the Page Has Expired popup or the popup in FF..

not a real hassle but still, i think we just have to bear it...


Fri, 06 Jan 2006 at 15:22:36 GMT Link

19.GolfGuy wrote:

This worked great. Thanks for the explanation.

Sun, 08 Jan 2006 at 09:45:34 GMT Link

20.v7n wrote:

Great information. Where's the tip jar? :)

Mon, 16 Jan 2006 at 01:02:29 GMT Link

21.Richard Lynch wrote:

There are many cases where the beginner has used POST for something that, really, could just as easily be GET without violating the RFC.

In those cases, switching to GET is probably the better solution.

The overhead of the HTTP re-direct headers and the browser thrashing back-and-forth can be painful on a busy server, and more difficult to follow when trying to debug a script.

The only caveat is that some servers limit the amount of data that GET can transmit even more than they limit POST data.

Of course, if the user action DOES change data on the server in a way that web-crawlers, bots, search engines etc should not, you MUST stick with POST.

Tue, 11 Apr 2006 at 23:22:42 GMT Link

22.Joseph Crawford wrote:

Yet another very informative article.

Thanks Chris :)

Thu, 10 Aug 2006 at 20:39:22 GMT Link

23.Francisco H. Perez wrote:

Well, what can I say, I have been looking for this solution for a while, and just today I implemented it and works awesome. Thanks.

Fri, 11 Aug 2006 at 16:34:53 GMT Link

24.Cagdas Cubukcu wrote:

Great solution!

Wed, 25 Oct 2006 at 18:41:10 GMT Link

25.Ben Mitchell wrote:

Excellent solution, thank so much. I have employed this on my site and with a recent download of a software update for Mac OS 10 (taking me to 10.4.8) I have found that it won't let multiple page forms to pass session information. Has anyone else experienced this problem?

Wed, 08 Nov 2006 at 16:18:09 GMT Link

26.sid nohcu wrote:

Thanks for the site.

PHP people gloss over issues and think PHP is self explanatory.

Like everything else I guess if you have familiarity.

Sat, 30 Dec 2006 at 11:51:00 GMT Link

27.Chris B wrote:

Great stuff Chris!

Have been polishing up on php security techniques, as I'm building an advanced site that needs to be as secure as I can possibly make it (no shared hosting, SSL, latest PHP version, etc) and your site has been great, not only in specific pointers, but more importantly in offering a nice cross-section of tips and techniques -- nothing worse than the feeling that you've *missed* something important that will come back and bite you in the ass someday...

Went and ordered a copy of your book directly! (proof that putting these articles online is good biz too!!!).



(another Chris...)

Sat, 20 Jan 2007 at 12:22:39 GMT Link

28.Chris Shiflett wrote:

Thanks very much for the kind words, Chris, and I'm glad you find this stuff helpful. :-)

Sat, 20 Jan 2007 at 16:13:51 GMT Link

29.Lukasz wrote:

Thank you Chris!

I have been struggling with this problem for some time and could not find cause nor remedy for it. Your article makes the problem and solution clear, is simple to follow at the same time being a nice read.


Lukasz (or Luke: whichever you prefer ;-)

Sun, 25 Feb 2007 at 23:17:08 GMT Link

30.Rooturaj wrote:

really helpful and enlightening article. got the hang of the thing in a shot.

thanks a bunch Chris

Thu, 05 Apr 2007 at 10:32:20 GMT Link

31.Craig Francis wrote:

Hi, thanks for the article.

One solution I use is something like the following...



if ($_POST['act'] != '') {


// Validation


// If valid, process data


// If valid, send to static thank you page

header('Location: http://...');

exit('goto <a href="...">next page</a>.');


// If invalid, continue script execution, and

// re-display the page with the fields re-filled.



<form action="./" method="post">


<input type="hidden" name="act" value="submit" />



Mon, 14 May 2007 at 20:44:26 GMT Link

32.Craig Francis wrote:

Sorry about the last post, forgot to add the formatting.

I've done a little bit more of a writeup here...


Sat, 19 May 2007 at 12:51:07 GMT Link

33.Bozzie wrote:

Thanks for this article, Chris. I solved my "Page has expired" problems and learned more about the inner workings of PHP sessions at the same time.



Thu, 31 May 2007 at 10:07:23 GMT Link

34.Kris Sebesta wrote:

This example uses a single page to do all three scripts above. It's very simple and works without any problems.



ini_set('session.cache_limiter', 'private');

$do = '';

if (isset($_GET['do'])) {$do = $_GET['do'];}

if ($do == 'post') {


if (isset($_SESSION['X'])) {

$_SESSION['X'] = $_SESSION['X'] + 1;

} else {

$_SESSION['X'] = 1;




<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">



<title>Untitled Document</title>

<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">





if (isset($_SESSION['X'])) {

echo $_SESSION['X'];

} else {echo 'No session X';}


<form action="TestX.php?do=post" method="post">

This is a test.

<input name="Test" id="Test" type="text" value="<?php if (isset($_SESSION['X'])) {echo $_SESSION['X'];} ?>" size="22" maxlength="22">

<input name="submitme" type="submit" value="Submit Me!">




Wed, 13 Jun 2007 at 07:05:04 GMT Link

35.why me wrote:

hi Chris, sorry disturbing you. I want to ask a question about php coding, hope you can help me. I make a form using 2 page. my problem is, when I edit my form at the first and second page, but before submit the form I decided to edit back my info at first page but the problem is the data are missing. can you help me with this problem.

here are some code that I use in my form.

    $ID = $_POST['ID];
    $_SESSION['ID] = $ID;
// this code at redirect page.
$v_ID = $_SESSION['ID'];
    if($v_ID == '' || $v_ID == NULL)
        $pln_query = mysql_query("SELECT * FROM pln_student WHERE IDStudent = '$v_IDStudent") 
        or die('Query failed: ' . mysql_error());
        $pln_query = mysql_query("SELECT * FROM pln_student 
        WHERE ID= '$v_ID' 
        or die('Query failed: ' . mysql_error());
// and this at the main page

Tue, 24 Jul 2007 at 17:57:44 GMT Link

36.Dave Ches wrote:

This article clearly doesn't explain the situation fully, since I can add session_start(); which produces the new headers as specified, browse further, then click the back button and NOT see the Page Expired message, but still see "hello world".

Mon, 30 Jul 2007 at 00:55:01 GMT Link

37.george john wrote:

great!!! solved the problem... thanks

Fri, 17 Aug 2007 at 11:17:35 GMT Link

38.Prasanna wrote:


Any better ways to handle post backs?

It's costing up a lot in terms of database connections and other resources whenever user hits F5 :(

Tue, 25 Sep 2007 at 18:42:32 GMT Link

39.Peter Easson wrote:

I use the header no cache stuff above, and I found the simplest way to prevent the "Warning:Page expired.." when using theBack button is simply one line of code.

After you have processed the POST array, and you're happy with the return result, simply add the following line in PHP

$_POST = array() // empty the POST array variable. Then there is nothing to post back

Wed, 03 Oct 2007 at 04:07:04 GMT Link

40.Bert wrote:

Kudo's for you, Chris. Well written and easy to understand.

Thu, 11 Oct 2007 at 22:50:17 GMT Link

41.Prasanna wrote:


That's cool. Let me try.

Fri, 19 Oct 2007 at 19:11:00 GMT Link

42.Anonymous wrote:

Very understandable article !

Thanks Chris.

Tue, 04 Dec 2007 at 10:53:20 GMT Link

43.Cris wrote:

That was nice! Thank you so much!

Wed, 09 Jan 2008 at 07:55:24 GMT Link

44.Israel Junior wrote:

Wow, at first I thought you were the Foo Fighters' guitarist :)

I'm getting freak with these warnings. Where I work most intranet systems have this problem and I was trying to solve them (unsuccessfully). Thanks to your guide my problems are gone.

By the way, nice blog. And it's now in my favorites :)

Wed, 20 Feb 2008 at 11:12:35 GMT Link

45.John Smith wrote:

From the php website:


In private mode, the Expire header sent to the client may cause confusion for some browsers, including Mozilla. You can avoid this problem by using private_no_expire mode. The expire header is never sent to the client in this mode.

Should I use it?

Fri, 25 Apr 2008 at 22:30:15 GMT Link

46.Uwe wrote:

Basically this is a design question. Your web application should be robust enough to handle each back and forward action from any web browser. Simple to a POST (or GET) to another display page isn't really the solution these days. You should use a MVC pattern at least. Sending input to a controller and let the controller decide what to do next - in general this is what the author has mentioned. So if you use a framework like Struts (for Java) or cackephp you will never have to struggle with these problems.

Tue, 24 Mar 2009 at 10:26:59 GMT Link

47.Baer wrote:

Thanks for the article, really helped me.

Using POSTback to one and the same routine is not an issue as long as you do not start a session in that routine: the back-button just works fine, no messages appearing. Whenever you need to start a session you can use the solution as suggested above.

Fri, 07 Aug 2009 at 21:43:58 GMT Link

48.Colin Cogle wrote:

I put a quick summary in my blog and linked it back here; hope you don't mind. I'm just grateful that you've shined some light on this mystery. No more will I have to explain to my boss that this is just normal behavior and not my fault.

Mon, 02 Nov 2009 at 05:19:06 GMT Link

Hello! What’s your name?

Want to comment? Please connect with Twitter to join the discussion.