Откуда берется заголовок Content-Type: nginx + php-fpm +9


AliExpress RU&CIS

Rocket science не будет. Если вы используете php-fpm, то скорее всего в связке с nginx. Простой вопрос: как в PHP получить значения HTTP заголовков запроса клиента?

  1. Например, стандартные Accept, Host или Referer?
  2. Знаете? Здорово! А как получить значение Content-Type, Content-Length?
  3. Ничем вас не удивить, а как получить значение произвольного заголовка, например X-Forwarded-For?

image

Как в PHP получить значения HTTP заголовков входящего запроса?


Всё очень просто (табличка сарказм). Нужно перейти на страницу документации переменной $_SERVER.
Переменная $_SERVER — это массив, содержащий информацию, такую как заголовки, пути и местоположения скриптов. Записи в этом массиве создаются веб-сервером.
Нет гарантии, что каждый веб-сервер предоставит любую из них;
сервер может опустить некоторые из них или предоставить другие, не указанные здесь.
Тем не менее многие эти переменные присутствуют в спецификации CGI/1.1,
так что вы можете ожидать их наличие.
Согласитесь звучит не очень обнадеживающе? Складывается ощущение, что это переменные Шрёдингера. На странице документации приводится ответ на первый вопрос.

$_SERVER['HTTP_ACCEPT']
$_SERVER['HTTP_HOST']
$_SERVER['HTTP_REFERER']

Ок, вроде бы всё просто, хоть на странице документации и не сказано про CONTENT_TYPE (правда есть небольшая подсказка комментария 2013 года), попробуем получить значение по аналогии.

$_SERVER['HTTP_CONTENT_TYPE']

К сожалению, такого ключа в массиве нет.

Ну да ладно, давайте посмотрим спецификацию CGI/1.1.
4.1.3. CONTENT_TYPE
If the request includes a message-body, the CONTENT_TYPE variable is
set to the Internet Media Type [6] of the message-body.

//…

There is no default value for this variable. If and only if it is
unset, then the script MAY attempt to determine the media type from
the data received. If the type remains unknown, then the script MAY
choose to assume a type of application/octet-stream or it may reject
the request with an error (as described in section 6.3.3).

//…

The server MUST set this meta-variable if an HTTP Content-Type field
is present in the client request header. If the server receives a
request with an attached entity but no Content-Type header field, it
MAY attempt to determine the correct content type, otherwise it
should omit this meta-variable.
Мы узнали ответ на второй вопрос.

$_SERVER['CONTENT_TYPE']
$_SERVER['CONTENT_LENGTH']

Перейдём к 3-му вопросу, продолжив чтение спецификации.
4.1.18. Protocol-Specific Meta-Variables

The server SHOULD set meta-variables specific to the protocol and
scheme for the request. Interpretation of protocol-specific
variables depends on the protocol version in SERVER_PROTOCOL. The
server MAY set a meta-variable with the name of the scheme to a
non-NULL value if the scheme is not the same as the protocol. The
presence of such a variable indicates to a script which scheme is
used by the request.

Meta-variables with names beginning with «HTTP_» contain values read
from the client request header fields, if the protocol used is HTTP.
The HTTP header field name is converted to upper case, has all
occurrences of "-" replaced with "_" and has «HTTP_» prepended to
give the meta-variable name.
The header data can be presented as
sent by the client, or can be rewritten in ways which do not change
its semantics. If multiple header fields with the same field-name
are received then the server MUST rewrite them as a single value
having the same semantics. Similarly, a header field that spans
multiple lines MUST be merged onto a single line. The server MUST,
if necessary, change the representation of the data (for example, the
character set) to be appropriate for a CGI meta-variable.

The server is not required to create meta-variables for all the
header fields that it receives. In particular, it SHOULD remove any
header fields carrying authentication information, such as
'Authorization'; or that are available to the script in other
variables, such as 'Content-Length' and 'Content-Type'.
The server
MAY remove header fields that relate solely to client-side
communication issues, such as 'Connection'.
А вот и ответ на 3-ий вопрос.

$_SERVER['HTTP_X_FORWARDED_FOR']

Тут же мы узнали, что спецификация просит не заполнять $_SERVER['HTTP_CONTENT_TYPE'], а использовать $_SERVER['CONTENT_TYPE'].

Как Content-Type попадет в переменную $_SERVER['CONTENT_TYPE']?


Перейдём ко второй части. Копнём чуть глубже, и посмотрим как веб-сервер (nginx) заполняет данными php массив $_SERVER.

Допустим мы решили поднять nginx + php-fpm через docker-compose

docker-compose.yaml
version: '3'

services:
  nginx_default_fastcgi_params:
    image: nginx:1.18
    volumes:
      - ./app/public:/var/www/app/public:rw
      - ./docker/nginx_default_fastcgi_params/app.conf:/etc/nginx/conf.d/app.conf:rw

  php-fpm:
    build:
      context: docker
      dockerfile: ./php-fpm/Dockerfile
    volumes:
      - ./app:/var/www/app:rw


Примерно так будет выглядеть nginx конфиг app.conf

server {
    listen 81;
    server_name server1.local;
    root /var/www/app/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php {
        fastcgi_pass php-fpm:9000;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        # file location /etc/nginx/fastcgi_params
        include fastcgi_params;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    error_log /var/log/nginx/app_error.log;
    access_log /var/log/nginx/app_access.log;
}

Здесь нужно обратить внимание на строчку include fastcgi_params;. Она подключает файл /etc/nginx/fastcgi_params, который выглядит примерно так

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

В этом месте как раз заполняется $_SERVER['CONTENT_TYPE']. А так же остальные значения указанные в спецификации

image.

И последний вопрос: Как остальные HTTP заголовки, например User-Agent попадают от nginx к php-fpm?

Всё просто, документация nginx даёт ответ.
Parameters Passed to a FastCGI Server

HTTP request header fields are passed to a FastCGI server as parameters. In applications and scripts running as FastCGI servers, these parameters are usually made available as environment variables. For example, the “User-Agent” header field is passed as the HTTP_USER_AGENT parameter. In addition to HTTP request header fields, it is possible to pass arbitrary parameters using the fastcgi_param directive.
Заметьте, здесь сказано, что HTTP заголовки передаются в приложение как HTTP_*. Но на самом деле два заголовка Content-Type и Content-Length, передаются по другому. Я бы назвал это ошибкой документации, но в ней есть слово usually, поэтому не будем придираться.

Выводы


1) Чтобы в php получить значение заголовка Content-Type/Content-Length нужно использовать $_SERVER['CONTENT_TYPE']/$_SERVER['CONTENT_LENGTH']. Для всех остальных заголовков $_SERVER['HTTP_*']

2) Я не знаю причину почему CGI выделил логику заголовков Content-Type/Content-Length. Возможно, для этого была весомая причина. Но результатом является куча неправильного кода программистов.

Например, на stackoverflow советуют вот так получить все HTTP заголовки

function getRequestHeaders() {
    $headers = array();
    foreach($_SERVER as $key => $value) {
        if (substr($key, 0, 5) <> 'HTTP_') {
            continue;
        }
        $header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
        $headers[$header] = $value;
    }
    return $headers;
}

Как не сложно заметить, заголовки Content-Type/Content-Length данный код не вернет. При этом ответ имеет 350+ лайков.

Похожий код можно найти и в документации php

<?php
if (!function_exists('getallheaders')) {
    function getallheaders() {
       $headers = [];
       foreach ($_SERVER as $name => $value) {
           if (substr($name, 0, 5) == 'HTTP_') {
               $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
           }
       }
       return $headers;
    }
}

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Оцените полезность статьи

  • 18,4%Я узнал много нового20
  • 32,1%Я узнал немного нового35
  • 49,5%Я не узнал ничего нового из этой статьи54




Комментарии (4):

  1. delphinpro
    /#22224200

    Сделаем var_dump($_SERVER) и узнаем что там есть.

    • /#22224920

      xdebug, не наш метод? )

  2. SlavikF
    /#22225930

    Из своего админского опыта:

    Есть вот такие переменные:
    fastcgi_param HTTPS $https if_not_empty;
    fastcgi_param SERVER_PORT $server_port;


    И обычно всё работает.
    Сложности возникают, если используется reverse proxy, в котором обслуживается HTTPS спереди того nginx в котором хостится сам сайт.
    Например у меня было несколько сайтов, — каждый в своём Докер-контейнере по HTTP. А в Интернет это публиковалось с помощью прокси jwilder/nginx-proxy + letsencrypt-nginx-proxy-companion

    Угадайте, что будет в fastcgi_param HTTPS? И в fastcgi_param SERVER_PORT?
    Там будет HTTP и порт 80.
    И Wordpress будет вам отдавать кривые redirect ссылки что-то типа HTTPS://web.site:80.

    Я это решал тем, что прописывал порт прямо в конфиге, и убирая fastcgi_param HTTPS:

    #fastcgi_param HTTPS $https if_not_empty;
    fastcgi_param SERVER_PORT 443;

    • VolCh
      /#22226016

      (X-)Forwarded-Proto и т. п. обычно решают это.


      А вообще мощный механизм fascgi_param в этом плане, хоть разные параметры подключения к Бд передавай в зависимости от разных факторов из запроса