решение задач

Работа с HTTP-заголовками в PHP

В этой заметке вы найдете ответы на следующие вопросы:

Как узнать заголовки запроса?

В этой вводной заметке мы рассмотрим стандартные функции PHP по работе с HTTP заголовками.

  1. header() Записывает строку в заголовки ответа
  2. headers_list() Возвращает массив заголовков ответа
  3. headers_sent() Проверяет отправлены ли заголовки ответа клиенту
  4. getallheaders() Возвращает массив заголовков запроса
  5. get_headers() Возвращает заголовки ответа удаленного сервера
Мы подробно рассмотрим все достоинства и недостатки этих функций.

Функция header()

PHP
void header( string string [, bool replace [, int http_response_code]] )

Функция header() записывает в массив заголовков текущего HTTP-ответа очередной заголовок. Функция имеет один обязательный аргумент string string, который должен содержать строку заголовка.
Второй необязательный аргумент bool replace включает или отключает режим перезаписи значения заголовка. Если аргумент имеет значение true, то новые заголовки будут заменять уже записанные в массив заголовки с тем же именем. Если аргумент имеет значение false, то новое значение уже имеющегося заголовка будет добавлено в предыдущий заголовок через запятую. Например:

PHP
header( 'Cache-Control: no-cache' );
header( 'Cache-Control: no-store', false );
В результате в заголовках ответа окажется один заголовок с двумя значениями
HTTP
Cache-Control: no-cache, no-store
Впрочем, если вам сразу известны все отправляемые значения этого заголовка, то проще записать их одной строкой:
PHP
header( 'Cache-Control: no-cache, no-store' );

Третий необязательный параметр int http_response_code может содержать код состояния (код ответа), согласно которому будет изменена строка состояния.

Однако, не смотря на то, что функция header() принимает строку заголовка в сыром виде, она не настолько либеральна, чтобы записывать в заголовки все, что вы ей передадите. Существуют определенные ограничения на формат записываемых заголовков. Если требования не выполняются, это в лучшем случае приведет к отсутствию ожидаемой вами строки в переданных клиенту заголовках. В худшем случае клиент получит ответ с кодом состояния 500 Internal Server Error.
Функции header() могут быть переданы два вида строк: строка состояния и HTTP-заголовки.
На передаваемые HTTP-заголовки налагается следующее ограничение: они должны содержать имя и значение заголовка, отделенные знаком двоеточия. Если отсутствует имя, значение или пропущено двоеточие, то такая строка не рассматривается как заголовок и игнорируется. Кроме того имя заголовка должно состоять только из символов ASCII. Никаких иных ограничений на имена и значения нет. Вы можете выдумывать собственные заголовки и отправлять их кому вздумается. Согласно стандарту протокола HTTP, неизвестные заголовки должны передаваться на всех участках цепи, а браузеры при интерпретации заголовков должны их игнорировать.

Одним из неоспоримых достоинств функции header() является то, что она может изменять строку состояния HTTP ответа. Строка состояния всегда стоит первой строкой в массиве заголовков и имеет следующий формат:

HTTP
HTTP/[версия протокола] [код состояния] [поясняющая фраза]

Версия протокола может быть как конкретной, например 1.0 или 1.1, так и общей, например 1.x. Однако при попытке записать конкретную версию протокола, я обнаруживал, что клиенту все равно отправляется общий шаблон 1.x. Возможно здесь виноват не PHP, а сервер Apache, который как-то обрабатывает заголовки перед отправкой.
Код состояния - это числовой код, соотнесенный с определенным типом ответа. Обработка несуществующих кодов состояния зависит от версии сервера Apache. Старые версии не позволяли отправлять некорректные коды состояния. При попытке передать неизвестный код состояния (например 430 или 600), клиент получает ответ с ошибкой:

HTTP
HTTP/1.x 500 Internal Server Error

Однако, в новой версии Apache 2 допустимо отправлять несуществующие коды состояний. К несуществующему коду состояния Apache 2 добавляет поясняющую фразу "OK".

Поясняющую фразу можно вообще не передавать. Если вы ее не передадите, то Apache сам допишет ее, на основании кода состояния. Если же вы передадите какую-то произвольную поясняющую фразу, она все равно будет переписана на стандартную фразу на основании переданного кода состояния.

У функции header() существует особое поведение при добавлении заголовка Location. Помимо добавления этого заголовка функция изменяет строку состояния на HTTP/1.x 302 Found. Но если вам необходимо установить другой код состояния, например 307 Temporary Redirect, то вам потребуется дополнительный вызов функции header(), чтобы изменить строку состояния. Не важно, будет это сделано до добавления заголовка Location, или после - изменения будут сохранены и отправлены клиенту. Но существует более изящный способ с использованием третьего аргумента int http_response_code. Заголовок location с кодом состояния 307 можно добавить одной строкой следующим образом:

PHP
header( 'Location: /redirect.php', 1, 307 );

В свете изменений в PHP 4.3.0, которые позволили изменять строку состояния таким способом, становится совершенно бессмысленным изменение строки состояния явным образом (разве что для наглядности). Ведь фактически, как мы увидели выше, функция header() реагирует только на код состояния, который вы ей передаете в строке состояния. А версию протокола и поясняющую фразу Apache все равно ставит по своему усмотрению. Недопустимые коды состояний в аргументе http_response_code обрабатываются точно так же, как и при передаче их в строке состояния. Более подробную информацию о кодах состояния без привязки к PHP можно прочитать в заметке Коды состояния протокола HTTP.

При использовании функции header() важно помнить, что запись заголовков ответа возможна только до тех пор, пока не начался вывод данных в выходной поток. Как только вы выводите данные на экран (конструкция echo, print, print_f и др. или же просто текст или пробелы вне тегов <?php ?>), вместе с этими первыми данными отправляются и заголовки ответа. Если после этого вы попытаетесь записать заголовок ответа, вы получите следующее предупреждение:

Warning: Cannot modify header information - headers already sent by

и далее сообщается файл PHP-скрипта и строка кода, в которой был начат вывод данных в выходной поток.

Функция headers_list()

PHP
array headers_list( void )

Функция headers_list() доступна лишь с пятой версии PHP. Она возвращает только те заголовки, которые были добавлены из PHP-скрипта. Если вы не прописывали никаких заголовков, функция вернет только заголовок X-Powered-By: PHP/5.2.10, и то не всегда. Конечно, иначе быть не может, ведь все остальные заголовки сервер прописывает уже после выполнения скрипта PHP.
Более того, эта функция не возвращает строку состояния, даже если она была изменена из PHP-скрипта.

Функция headers_sent()

PHP
bool headers_sent( [string &file [, int &line]] )

Отладочная функция, сообщающая о том, отправлены ли клиенту заголовки текущего HTTP-ответа. Два необязательных параметра позволяют записать в переданные переменные имя PHP скрипта (первый аргумент) и номер строки (второй аргумент), где начался вывод данных в выходной поток, с которыми были отправлены заголовки.

ОБРАТИТЕ ВНИМАНИЕ, что эти переменные нельзя объявлять, т.к. в действительности передаваемые аргументы являются переменными, которые PHP интерпретатор объявит сам, а не ссылками. Это странная особенность продиктована необходимостью. Ведь запись в эти переменные происходит не в момент вызова функции, а в момент начала вывода данных, а это было до вызова функции. Т.е. интерпретатор PHP запоминает эти переменные перед выполнением всего скрипта. Таким образом, если вы объявите или присвоите значения этим переменным, вы тем самым затрете уже записанные в них значения. Напоминает «Алису в Зазеркалье», но придется с этим смириться.

Функция getallheaders()

PHP
array getallheaders( void )

Эта функция работает только в том случае, если PHP работает в качестве модуля Apache. Чтобы этот факт был более очевиден, начиная с PHP 4.3.0 эта функция получила псевдоним apache_request_headers()

Функция возвращает ассоциативный массив со строками заголовков текущего HTTP-запроса. Ключами массива служат имена заголовков, а значения - содержание заголовков. К сожалению, в массиве не содержится строка запроса с методом запроса, запрашиваемым ресурсом и клиентской версией протокола HTTP. Строка запроса - это первая строка заголовков запроса, например: GET /index.php HTTP/1.1
Однако эту информацию можно почерпнуть из других источников, например из переменных окружения сервера. Переменные окружения создаются сервером и доступны всем его компонентам, включая PHP. В PHP они доступны через глобальный массив $_SERVER. В этом массиве содержится более исчерпывающая информация, нежели предоставляет функция getallheaders(). Нас будут интересовать те переменные окружения, которые содержат информацию из заголовков HTTP-запроса.

  1. $_SERVER['REQUEST_METHOD'] Метод текущего HTTP-запроса, например: GET, HEAD, POST, PUT.
  2. $_SERVER['REQUEST_URI'] Запрашиваемый ресурс, например: /index.php?p=2&s=1
  3. $_SERVER['QUERY_STRING'] Строка GET-параметров запрашиваемого ресурса, например: p=2&s=1
  4. $_SERVER['REQUEST_TIME'] Время из заголовка запроса Date, приведенное к формату Timestamp
  5. $_SERVER['HTTP_HOST'] Содержимое заголовка запроса Host

... а так же все переменные окружения с именами "HTTP_ИМЯ_ЗАГОЛОВКА", которые созданы сервером на основании заголовков текущего HTTP-запроса. Примеры других переменных окружения, содержащих заголовки запроса: $_SERVER['HTTP_KEEP_ALIVE'], $_SERVER['HTTP_CACHE_CONTROL'], $_SERVER['HTTP_REFERER']. Эти элементы могут и отсутствовать в массиве, если клиент не передал соответствующие заголовки запроса.

К сожалению, чего нельзя узнать, так это клиентской версии HTTP протокола, которая содержится в строке запроса.

Одним из достоинств функции getallheaders() перед массивом $_SERVER является то, что она предоставляет имена заголовков в их первозданном виде, тогда как в массиве $_SERVER к имени заголовка прибавляется префикс "HTTP_". Однако учитывая тот факт, что имена заголовков являются регистронезависимыми, клиент может передать имена в нижнем регистре, а может с заглавными буквами, либо, в исключительных случаях, в любом экзотическом варианте регистров. Этим осложняется поиск интересующих заголовков в массиве, который выдает функция getallheaders(). С массивом $_SERVER такой проблемы нет - там все ключи массива всегда в верхнем регистре.

Функция get_headers()

PHP
array get_headers( string url [, int format] )

Функция предназначена для получения от удаленного сервера списка всех заголовков, включая строку состояния. В первом обязательном параметре указывается адрес ресурса, к которому делается запрос. Адрес должен предваряться схемой HTTP-протокола http://. Второй необязательный параметр позволяет указать, в каком виде возвращать массив: в виде нумерованного массива-списка (int format = 0) или в виде ассоциативного массива (int format = 1), где ключами массива служат имена заголовков, а значения - содержание заголовков. В последнем случае строка состояния все равно находится в элементе с индексом 0. По умолчанию второй параметр принимается равным нулю.

До версии PHP 5.3.0 эта функция обладает одним недостатком, который ограничивает ее применение исключительно отладочными целями. По логике протокола HTTP эта функция должна была бы осуществлять запрос методом HEAD, т.к. ее интересуют только заголовки. Однако при вызове этой функции PHP посылает удаленному серверу запрос следующего вида:

HTTP
GET /index.php HTTP/1.0
Host: http11.ru

Естественно, на этот запрос удаленный сервер возвращает не только заголовки, но и содержимое ресурса, которое по сравнению с заголовками занимает объем порой в сотни раз больший. Таким образом, делается вопиюще не рациональный запрос, бессмысленно нагружающий оба сервера и сеть маршрутизаторов и шлюзов между ними. Разработчики протокола HTTP всячески борются за ослабление растущей нагрузки на сеть, а программные решения наподобие функции get_headers(), а точнее их бездумное использование сводят эти усилия на нет.

Однако в версии PHP 5.3.0 этот недостаток был устранен с появлением функции stream_context_set_default(). Еще начиная с версии 5.1.3 функция get_headers() использует stream context по умолчанию. Но настройки stream context стало возможным менять только с версии 5.3.0, например изменить метод запроса.

PHP
stream_context_set_default(
    array( 'http' => array( 'method' => 'HEAD' ) )
);

Разумеется делать это нужно перед вызовом функции get_headers().

Метод HTTP-запроса HEAD был введен еще в HTTP/1.0 Этот метод был разработан именно для таких случаев. В спецификации RFC 1945 особо отмечено, что этот метод идентичен методу GET за исключением того, что он не возвращает содержимого ресурса. Правда в спецификации RFC 2616 для протокола HTTP/1.1 условие возврата идентичных заголовков в сравнении с методом GET имеет уровень требования SHOULD, т.е. желательно, но не сторого обязательно (подробнее см. заметку метод HEAD). Наиболее популярный сервер Apache поддерживает это требование. Смею предположить, что и многие другие серверы тоже его поддерживают.

Комментарии:

  1. 18 января 2013, 09:54

    Иван

    комментарий No. 5

    Благодарю за полезный ресурс.

    Ответить на комментарий..

  2. 25 декабря 2012, 23:58

    Антон

    комментарий No. 4

    Спасибо!
    Хороший дизайн и самое главное нужная информация. Расписано как-будто документация.
    Для меня было настоящим шоком узнать, что get_headers работает методом GET О_О
    Полез на сайт PHP в документацию и увидел, что оказывается, есть способ заставить ее работать методом HEAD одной строкой. Автор может дополните?

    • 26 декабря 2012, 13:19

      Автор

      ответ No. 1

      Спасибо, за информацию. Обязательно отредактирую статью в соответствии с новыми обстоятельствами, как только проверю и протестирую данный метод.

    Ответить на комментарий..

  3. 18 ноября 2012, 17:47

    Дмитирий

    комментарий No. 3

    Почему всегда осмысленный материал как этот всегда сложно найти?

    • 19 ноября 2012, 12:49

      Автор

      ответ No. 1

      Этот вопрос надо переадресовать поисковым системам. Нынче раскрутка решает все. А осмысленная у тебя информация или нет - дело второе.
      Я для себя решил, что хороший сайт в раскрутке не нуждается. Это моя позиция.

    Ответить на комментарий..

  4. 28 декабря 2010, 21:34

    Денис

    комментарий No. 1

    Очень интересная статья

    Ответить на комментарий..

Ваш комментарий