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

    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;
        }
    }
    

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

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

    • 17,8%Я узнал много нового23
    • 34,9%Я узнал немного нового45
    • 47,3%Я не узнал ничего нового из этой статьи61

    Похожие публикации

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 550 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

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

      +10
      Сделаем var_dump($_SERVER) и узнаем что там есть.
        –2
        xdebug, не наш метод? )
          0
          xdebug для ньюфагов. Олды юзают var_dump.
            0
            и блокнот )
        +2
        Из своего админского опыта:

        Есть вот такие переменные:
        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;

          0

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


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

          0
          А еще есть функция apache_request_headers, которая вернет все заголовки.
            0
            Посмотрел из интереса её реализацию
            php-7.4.12/sapi/cgi/cgi_main.c
            PHP_FUNCTION(apache_request_headers) /* {{{ */
            {
            	if (zend_parse_parameters_none()) {
            		return;
            	}
            	array_init(return_value);
            	if (fcgi_is_fastcgi()) {
            		fcgi_request *request = (fcgi_request*) SG(server_context);
            
            		fcgi_loadenv(request, sapi_add_request_header, return_value);
            	} else {
            		char buf[128];
            		char **env, *p, *q, *var, *val, *t = buf;
            		size_t alloc_size = sizeof(buf);
            		zend_ulong var_len;
            
            		for (env = environ; env != NULL && *env != NULL; env++) {
            			val = strchr(*env, '=');
            			if (!val) {				/* malformed entry? */
            				continue;
            			}
            			var_len = val - *env;
            			if (var_len >= alloc_size) {
            				alloc_size = var_len + 64;
            				t = (t == buf ? emalloc(alloc_size): erealloc(t, alloc_size));
            			}
            			var = *env;
            			if (var_len > 5 &&
            			    var[0] == 'H' &&
            			    var[1] == 'T' &&
            			    var[2] == 'T' &&
            			    var[3] == 'P' &&
            			    var[4] == '_') {
            
            				var_len -= 5;
            
            				if (var_len >= alloc_size) {
            					alloc_size = var_len + 64;
            					t = (t == buf ? emalloc(alloc_size): erealloc(t, alloc_size));
            				}
            				p = var + 5;
            
            				var = q = t;
            				/* First char keep uppercase */
            				*q++ = *p++;
            				while (*p) {
            					if (*p == '=') {
            						/* End of name */
            						break;
            					} else if (*p == '_') {
            						*q++ = '-';
            						p++;
            						/* First char after - keep uppercase */
            						if (*p && *p!='=') {
            							*q++ = *p++;
            						}
            					} else if (*p >= 'A' && *p <= 'Z') {
            						/* lowercase */
            						*q++ = (*p++ - 'A' + 'a');
            					} else {
            						*q++ = *p++;
            					}
            				}
            				*q = 0;
            			} else if (var_len == sizeof("CONTENT_TYPE")-1 &&
            			           memcmp(var, "CONTENT_TYPE", sizeof("CONTENT_TYPE")-1) == 0) {
            				var = "Content-Type";
            			} else if (var_len == sizeof("CONTENT_LENGTH")-1 &&
            			           memcmp(var, "CONTENT_LENGTH", sizeof("CONTENT_LENGTH")-1) == 0) {
            				var = "Content-Length";
            			} else {
            				continue;
            			}
            			val++;
            			add_assoc_string_ex(return_value, var, var_len, val);
            		}
            		if (t != buf && t != NULL) {
            			efree(t);
            		}
            	}
            }
            /* }}} */
            


            Логика как и описано выше: взять заголовки HTTP_* и добавить к ним CONTENT_TYPE/CONTENT_LENGTH. То есть под капотом у всех одно и тоже.

            Готовых решений очень много:
              +1
              Ну а откуда другой реализации взяться? Кроме как из CGI их никак не достать.

              Полифилы и прочие пакеты нужны были для древних версий пыха, начиная с 7.3 эта функция идет в стандартной поставке.
            0
            комментарий перенесён в ветку выше

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

            Самое читаемое