Wherein I occasionally rant on various topics including, but not limited to, PHP, Music, and whatever other Topics I find interesting at the moment, including my brain tumor surgery of August 2010.

Wednesday, June 13, 2007

PHP header location redirect refresh

I haven't posted much, but at php|tek 2007, Chris Shiflett actually said he liked my rant, and that I should post more.

Well, hey, if Chris Shiflett says I oughta do something, I listen.

So, today's rant is about PHP header Location hacks.

First, let's be sure everybody understands what it is:

header("Location: http://example.com");

will re-direct the browser to the URL example.com

Specifically, the PHP tells Apache to issue to the browser a 301 Redirect header to that URL, and then the browser gets that 301 Rediret header and automatically tries to visit that URL.

As pointed out on PHP-General, PHP actually sends a 302 Temporarily Moved rather than a 301 Permanently moved. The rest of the rant still applies, as I simply mis-typed 301 for 302 anyway.

Now this seems really cool at first, perhaps because it is really cool, when used for an appropriate problem.

Unfortunately, many PHP scripters are using this as an Idiom or as a Programming Construct with things like:

if (!logged_in()){
header("Location: login.php");

Now, this does "work" but there are several problems with it.

First and foremost, the HTTP Specs require a complete URI for the location. And while this fragment of a URI might "work" on most browsers, it will, sooner or later, totally mess you up when the browser mis-interprets this.

Specifically, some versions of IE will do the redirect, but won't POST the original data as a redirect should, with this URI fragment. Use the full URL, and IE actually does the right thing, and redirects with all the POST data intact.

You shouldn't use an incomplete URI just because "it works" any more than you should use non-compliant HTML just because it works.

But let's look at this header("Location: ") in a slow-motion instant replay:
User requests page X
Browser uses dog-slow Internet to contact web server.
Web server receives request, hands it off to PHP
PHP responds over dog-slow Internet with 301 Redirect to login.php.
Browser interprets 301 Redirect, hopefully correctly.
Browser uses dog-slow Internet to contact web server.
Web server receives request for login.php, hands it off to PHP
PHP finally spits out the login form.


Maybe you should consider just doing this instead:

if (!logged_in()){
require 'login.php';

And look, ma, no extra round trip through the dog-slow Internet.

Plus, you're not wasting an HTTP connection, which, on a busy server, is a precious resource. Presumably you'd like your website to be popular enough to be busy someday, right?

You're going to end up reading the login.php file in the end anyway. Why would you spend all that time going back and forth to the browser to do it?

And include 'login.php' is no more difficult to read/understand/maintain than the header().

Reserve a header("Location: xxx") for when you really need it: When a document has actually moved and the URL is being retired.

Don't use header("Location: xxx") as a Programming Construct in place of simple if/else and include logic.

PS This same rant applies to the header("Refresh: ") usage as well. Though why you'd want to use that instead of Location: is beyond me, and probably the subject of another rant...


Eric said...

I think another valid use is the PRG pattern. Ever since I implemented this on my forms I haven't had any problems with duplicates or cache expired messages.


Richard Lynch said...

Unfortunately, in some browsers, if a user intentionally hits "Stop" and "Back" and "Refresh" often enough, they'll probably be able to re-post the same input twice.

This means one still has to catch or avoid that somehow anyway, so you might as well do whatever it takes to catch/avoid it, and not do the re-direct.

Wesley said...

I do agree with eric in this case the Post Redirect Get is a good way to stop most double submissions.

We do use this at work but thats not to say work does everything right.

Other reasons I use redirects to say that content really has permanently moved. My reasoning about this is hopefully some website indexing actually uses headers.

Another thing is that well hopefully people notice the changed url. This means that they can use this information in the future. I feel a URL should always return the same page if that can be helped and at least redirecting people to the login.php makes them see "hey I'm on the login page" rather than this page is different to what I bookmarked.

Another thing that gets my goat with people not using these headers enough is the standard 404. Look if your going to remove a file from your webserver then use 410 Gone as the response.

And not your fault but this really bugs me. Why is this page I'm commenting on in spanish. I live in spain but my browser is sending its prefered language as english. Since when has IP to location been a good way to choose language?

rtconner said...

so what is a better way to stop duplicate data posting because the user hit refresh? not a big fan of people posting problems but no solutions.

Richard Lynch said...

Generate a one-use token and include it as a hidden attribute.

When you do the INSERT (successfully, with error-checking) mark that token as "used"

If the user tries to re-use the dead token, you can take whatever action is appropriate for your business process -- silently ignore it, error out, provide alternative options, or even go back to the original form and ask if they really really want two of the same thing in the database, and let them over-ride your "rules" if that's really how the world works.

darrell said...

Thanks for this. I've just implemented a redirect for a site which has an SSL certificate, and I need to redirect users to "https://www.sabrehealth.com". It's just a hack but it works. (I read the url and if the http doesn't contain an "s" I add one and redirect to the new url.)

I've been advised that using an .htaccess file is a better way to do this, but I don't know a damn thing about .htaccess files.

Your thoughts?

andy said...

1. Thanks for a thought provoking rant,
2. I'm currently using a location redirect to do an initial check to see whether cookies are being accepted (so enabling session persistence). Can't see anyway of avoiding this- am I right? Mine is an adapt of a snippet at www.phpit.net/article/php-cookies-good-mix/3/
3.Ta to richard_l for the use-once token idea
4. Yes, to Darrell; you could be using .htaccess and mod-rewrite. I'm just learning about this and will post up some links if you're still interested...

Bartomedia said...

I have a solution to the stop, redirect and back button problem. Using redirects I have sorted out the issue which is for http://viagraprescription.co.uk altho I have no tutorial yet I will be posting one soon at http://bartomedia.blogspot.com/ if you find this intresting please check back there over the next few week

marcus said...

Thanks for that point you make there. I must admit I don't use a full URL (never noticed on my test browsers) so I will go back over those.

Another point worth mentioning, is if you set a session variable in the script and then send a redirect header, if you not use "exit()" or "die()" after that, your session variable won't be set in the next page. Hope this bit of info saves someone the hours wasted figuring that out!

Net Web Logic

mapperguy said...

Thanks for the good advice, all.

Rich, I'm wondering about how to combine your super and simple method with the "refresh: delay" that's possible using the "header" hack.

I want to post a message to the first page indicating that "your action has been successful and you will be redirected to the next page in a moment..." I can do this successfully with the header hack.

Any thoughts about how this can be accomplished without the header hack?

Richard Lynch said...

To get your session data saved before you re-direct, use:
right before you do the re-direct.

If you really want them to see HTML message about the re-direct, you have to do that client-side with Meta tags (or javascript, or, I guess, the new-fangled refresh: delay)

Michael said...

I think that the header might be valid for the reason that now I am displaying the redirected page at a url that is not the page. I might be displaying login.php as /membersonly/home.php.

I do like that your way requires less bandwidth; however, it doesn't seem to actually send the user back to the page they should be on, instead it brings that page to them under a different name.

blogger said...

With php I actually have to have a php page hosted on the domain to be able to refresh, correct?
Is this better than using .htacess.

srividya jyothirmai said...

Can we send more than one variable to header in php? Let me knw sir...

dennis iversen said...

This small function for PHP keeps control of your clients post request without making the clients browser complain about 'reposting'. This means that the client can reload, move backward and forward in history when posting a form.


rsp said...

I must humbly disagree with Rich. Even though it's not ideal, the redirect header is a viable way to establish the correct page location for page history navigation purposes.

If you start showing pages that don't correspond with the URL you create two problems. First, obviously the page doesn't correspond with the URL. Second and perhaps more annoying, the Back button navigation can become screwed up too.

The example of requiring logon is too simplistic to be a valid argument. In the real world web applications are much more complex and the tough problem is that the user will have visited page(s) that are now no longer valid (e.g. they've logged off but their browser history is intact). So they can try to access these pages using the browser Back button or browser page history list. I've seen people try to fix up the browser history with some very clever hacks to solve this problem, but then it becomes a browser-dependent issue and it's virtually impossible to keep up as browsers evolve.

For situations where a page is no longer valid due to the user's session state, using the redirect header can be rationalized because the page really isn't there anymore even though it's in their browser history. So using redirect to get them to the correct page is appropriate.

Tharson said...

I'm teaching myself some PHP basics, and a tutorial was telling me to use header("Location: blahblah.php") but instead it would just reload the page, until I switched to request("blahblah.php").

Roman said...

Sometimes you really need to use something different from Location-header, especially when headers are already sent to the user. In this case we can use JavaScript approach:
<script language="javascript">
Or you can use Meta-tag redirection.

I described these approaches as well as Location-header approach in my article here - PHP: Redirection Overview

Thank you!

Anirudh said...

thanx for the help..that was a reaaly good article

brothercake said...

Just a quick addition to this long-running thread!

The various advice given here on handling POST redirects is contrary to HTTP specification -- requests that contain POST data should not be automatically redirected at all.