Przekierowania HTTP

W ostatnim okresie czasu intensywnie pracuję nad pewnym projektem w PHP. Prace te pozwoliły mi się zapoznać dokładnie z różnymi technikami przekierowywania przeglądarek do żądanych zasobów. W tym poście chciałbym omówić główne przekierowania zdefiniowane w protokole HTTP; przynajmniej jedno z nich powinno być powszechnie znane programistom PHP. Wpis ten oparty jest o post "The anatomy of server sided redirect" na blogu Sebastian's Pamphlets, specyfikacji HTTP/1.1, której wydrukowany fragment trzymam obok siebie oraz osobistych doświadczeniach. Przekierowania HTTP Jak wiadomo, fundamentalny dla WWW protokół HTTP zawiera kody odpowiedzi (opisane w sekcji 10 specyfikacji RFC2616), którymi serwer WWW informuje przeglądarki o tym, co przeglądarki mają dalej robić. Nas w tym miejscu interesują kody serii 3xx (sekcja 10.3 RFC). Zadaniem tych kodów jest przekierowanie przeglądarki pod inny adres docelowy. Kody te mają też wpływ na crawlery wyszukiwarek, które przecież starają się udawać ludzi. Powiemy sobie o:
  • HTTP 301 Moved Permanently
  • HTTP 302 Found
  • HTTP 303 See Other
  • HTTP 307 Temporary Redirect
Gdy serwer dokonuje przekierowania to oprócz odpowiedniego kodu statusu i opisu wysyła także nagłówek Location zawierający adres, pod który ma udać się przeglądarka. Przykładowa odpowiedź-przekierowanie HTTP, które przeniesie przeglądarkę na stronę Google, może wyglądać następująco:
HTTP/1.1 307 Temporary Redirect
Location: http://www.google.pl
HTTP/1.1 301 Moved Permanently Kod ten tworzy permanentne przekierowanie pod adres docelowy. To tak, jakbyśmy powiedzieli przeglądarce: "URI, o które prosiłaś znikło. Być może nigdy go nie było, a na pewno nigdy nie będzie. Nie będę dostarczać żadnej treści dla zapytania o to URI, więc zaktualizuj swoje zakładki podanym niżej adresem i nie zawracaj mi już więcej głowy. Nigdy więcej.". Przekierowania 301 używamy najczęściej przy przeniesieniu całej strony, gdy chcemy poinstruować ludzi i crawlery o zmianie lokalizacji. W szczególności powinniśmy to zrobić, jeżeli zależy nam na utrzymaniu dotychczasowej pozycji strony w wyszukiwarkach. Google rozumie kod 301, ale stronę trzeba przenosić z głową. Kod 301 jest przekierowaniem 'na zawsze' - URI raz oznaczone tym numerem nie powinno być nigdy więcej używane w innym celu. To znaczy, że mamy obowiązek go utrzymywać - przynajmniej jeśli zależy nam na Page Rank - tak długo, jak to tylko możliwe (tak, trzeba płacić i za starą domenę ;)). Nie powinno się też nigdy zmieniać miejsca, do którego odsyła to przekierowanie. Kodu 301 powinniśmy używać też do tzw. sprowadzenia adresu do postaci kanonicznej (pozwala uniknąć problemów z wyszukiwarkami, w tym podwójnego widzenia treści dla adresów z www i bez www). Przykład użycia pliku .htacccess i mod_rewrite do zapewnienia postaci kanonicznej adresu:
RewriteEngine On
RewriteCond %{HTTP_HOST} !^example\.com [NC]
RewriteRule (.*) http://example.com/$1 [R=301,L]
Należy zwrócić uwagę na fragment R=301 w ostatniej linijce - oznacza to, że do zmiany adresu zostanie użyte przekierowanie 301. Aby przekierowanie 301 zostało wykonane poprawnie musimy wysłać przeglądarce tekst z kodem i statusem - w przeciwnym razie wykonane zostanie domyślne przekierowanie 302 Found, o którym niżej. Tyczy się to szczególnie PHP, gdzie już chyba zwyczajem stało się pisanie:
Header('Location: '.$adres);
Taki kod wykona przekierowanie 302. Poprawnie sformułowane w PHP przekierowanie 301:
Header('HTTP/1.1 301 Moved Permanently');
Header('Location: '.$adres);
HTTP/1.1 302 Found Przekierowanie 302 jest chyba najbardziej nadużywanym przekierowaniem w sieci - głównie z powodu tego, że jest to przekierowanie domyślne, które zostanie wykonane jeżeli samodzielnie nie wykonaliśmy innego przekierowania. Problemy dotyczą głównie niewłaściwego używania 302 w miejscu, gdzie powinno być 301 - częściowo z niechlujstwa, a częściowo - wg Simon's Pamphlets - z różnych mitów rozpowszechnianych w środowisku SEO. Pomyłkowe użycie 302 zamiast 301 jest niegroźne zarówno dla człowieka jak i przeglądarki, ale może wyrządzić krzywdę pozycji strony w wyszukiwarce. Kod 302 Found (w HTTP/1.0 zwany 302 Moved Temporarily) służy do wskazania tymczasowej lokalizacji zasobu. Ponieważ jednak przekierowanie może się zmieniać, specyfikacja sugeruje, by klienci dalej używali tego adresu, którym przyszli, a nie tego, który dostaną z przekierowania. Jeżeli chce się z jakiegoś powodu odesłać użytkownika na inną stronę - czy to wewnętrzną, czy zewnętrzną w stosunku do naszego serwisu - to możemy użyć właśnie tego kodu. W PHP wygląda to tak:
//Wersja HTTP/1.0
Header('HTTP/1.0 302 Moved Temporarily');
Header('Location: '.$adres);

//Wersja HTTP/1.1
Header('HTTP/1.1 302 Found');
Header('Location: '.$adres);
Poza niewielkimi różnicami w specyfikacji dotyczącymi cache'ingu kody 302 obu wersji robią w zasadzie to samo. I chociaż dotychczasowe (9.3) RFC (10.3.3) nie pozwalały zmieniać przy przekierowaniu metody zapytania (np. z POST na GET), większość przeglądarek to właśnie robi (przeważyły prawdopodobnie względy bezpieczeństwa) - kod 302 jest interpretowany tak jak 303. Aby można było rozróżnić, kiedy serwer chce wykonać przekierowanie ze zmianą metody zapytania na GET a kiedy bez zmiany, wprowadzono w HTTP/1.1 dwa dodatkowe kody - 303 See Other i 307 Temporary Redirect. Ponieważ przeglądarki oraz crawlery potrafią już 'rozmawiać' protokołem HTTP/1.1, to poza szczególnymi przypadkami gdy z jakiegoś powodu musimy 'bawić się' w HTTP/1.0 możemy spokojnie korzystać z nowych kodów. HTTP/1.1 303 See Other Według RFC2616 (10.3.4) celem istnienia tego kodu jest przede wszystkim umożliwienie wynikom zapytania POST możliwości przekierowania do innych zasobów. W momencie otrzymania kodu 303 przeglądarka powinna zmienić metodę zapytania na GET. Poprawne użycie w PHP:
Header('HTTP/1.1 303 See Other');
Header('Location: '.$adres);
Kodu tego powinniśmy używać w skryptach wywoływanych zapytaniem POST - po przetworzeniu żądania powinny one wykonywać przekierowanie 303 na stronę, która wyświetli nam wyniki. Dzięki temu, gdy użytkownik odświeży stronę, zapytanie POST (mogące mieć przecież jakieś skutki uboczne!) nie zostanie wykonane ponownie (Firefox w takich sytuacjach pyta, czy ponownie wysłać dane). Jest to o tyle istotne, że zgodnie ze specyfikacją do wprowadzania zmian (edycja, usuwanie, itp.) w zasobach strony powinniśmy używać metody POST, nie GET (celowo pomijam tutaj PUT i DELETE - architektura REST, która je wykorzystuje, nie jest jeszcze aż tak popularna). HTTP/1.1 307 Temporary Redirect Kod 307 to nasz najlepszy przyjaciel, gdy potrzebujemy wykonywać przekierowania do tymczasowych zasobów. W przeciwieństwie do 303 nie zmienia metody zapytania - kod 307 zachowuje się tak, jak powinien był się zachowywać w przeglądarkach 302. To właśnie tym kodem chcemy się posługiwać, więc warto zadbać o jego poprawne wysłanie, żeby przypadkiem nie wyszedł nam 302:
Header('HTTP/1.1 307 Temporary Redirect');
Header('Location: '.$adres);
Jednym z zastosowań przekierowania 307 jest przekierowywanie kanałów RSS/Atom do usługi FeedBurner. Podsumowanie - kiedy używamy którego kodu?
  • HTTP 301 Moved Permanently - gdy zasób został na stałe przeniesiony pod inny adres. Przekierowania 301 powinniśmy utrzymywać włączone tak długo, jak to możliwe i nigdy nie poddawać ich recyclingowi.
  • HTTP 302 Found - gdy musimy borykać się z protokołem HTTP/1.0.
  • HTTP 303 See Other - gdy skrypt, który otrzymał zapytanie POST chce wyświetlić jakieś wyniki - powinien wtedy wykonać przekierowanie 303 na stronę z wynikami. Unikniemy dzięki temu ponownego wysłania zapytania POST przy odświeżeniu strony.
  • HTTP 307 Temporary Redirect - wszystkie sytuacje, gdy robimy tymczasowe przekierowania. Skracanie nazw URI, odesłanie do innej strony, przekierowanie RSS/Atom na FeedBurnerm itd.
Dodatkowe informacje
  • Omówione przekierowania operują bezpośrednio na poziomie protokołu HTTP. Oznacza to, że możemy ich użyć do przekierowywania np. obrazków osadzanych w tagach <img>.
  • Osobom używającym .htaccess do sprowadzania adresu do postaci kanonicznej (w tym np. usuwanie bądź dodawanie 'www' przed nazwą) polecam upewnić się, że używają przekierowania 301 a nie domyślnego 302.
Zainspirowany Gynvael'em i innymi postanowiłem utworzyć też dział Code Snippets, w którym znajdować się będą różne drobne i - miejmy nadzieję - przydatne fragmenty kodu. Jako pierwszy umieszczam drobną klasę PHP realizującą u mnie w projekcie przekierowania oraz obsługę wybranych kodów 4xx i 5xx.