mod_rewrite не отправляет Vary: accept-language, когда RewriteCond совпадает

У меня есть правило перезаписи, которое перенаправляет на /, если отсутствует язык принятия, и кто-то пытается посетить ?lang=en, Работает нормально, за исключением возвращаемых заголовков. Vary: accept-language отсутствует в ответе.

RewriteCond %{HTTP:Accept-Language} ^$  
RewriteCond %{QUERY_STRING}         ^lang=en  
RewriteRule ^$                      http://www.example.com/?     [R=301,L]

Документация Apache определяет:

Если в условии используется заголовок HTTP, этот заголовок добавляется в заголовок Vary ответа, если условие оценивается как истинное для запроса. Не добавляется, если условие оценивается как ложное для запроса.

Условия определенно совпадают и перенаправляют, поэтому я не понимаю, почему Apache не добавляет разные языки. Можно понять, почему это было бы настоящей проблемой, если бы прокси-сервер кэшировал то, что? Lang = en всегда перенаправляет на / независимо от отправляемого заголовка accept-language.

3 ответа

Решение

После того, как вы заглянули в убогий живот системы обработки запросов Apache, оказалось, что документация несколько вводит в заблуждение... Но прежде чем я углублюсь в объяснение, из того, что я могу сказать, вы зависите от Apache в этом.

Проблема клиента

Во-первых, имя заголовка не будет добавлено в заголовок ответа Vary, если оно не отправлено клиентом. Это связано с тем, как mod_rewrite создает значение для этого заголовка внутри.

Он ищет заголовок по имени, используя apr_table_get(), таблица заголовка запроса и имя, которое вы указали:

const char *val = apr_table_get(ctx->r->headers_in, name);

Если name не является ключом в таблице, эта функция вернет NULL, Это проблема, потому что сразу после этого происходит проверка val:

if (val) {
   // Set the structure member ctx->vary_this
}

ctx->vary_this используется на RewriteCond основа для накопления имен заголовков, которые должны быть собраны в окончательный заголовок Vary *. Поскольку при отсутствии значения присвоение или добавление не произойдет, ссылочный (но не отправленный) заголовок никогда не появится в Vary, В документации явно не говорится об этом, поэтому это может быть, а может и не быть тем, что вы ожидали.

* В сторону, NV (без изменений) флаг и функция игнорирования при сбое реализуется настройкой ctx->vary_this в NULL , предотвращая его добавление в заголовок ответа.

Тем не менее, возможно, что вы отправили Accept-Language, но он был пустым. В этом случае пустая строка пройдет вышеупомянутую проверку, а имя заголовка будет добавлено в Vary с помощью mod_rewrite из того, что описано выше. Имея это в виду, я использовал следующий запрос для диагностики происходящего:

Пользователь-агент: Fiddler
Принять: текст /html, приложение /xhtml+xml, приложение /xml;q=0,9,*/*;q=0,8
Accept-Language: 
Accept-Encoding: gzip, выкачать
Accept-Charset: ISO-8859-1,utf-8;q=0,7,*;q=0,7
Keep-Alive: 115
Подключение: keep-alive
Host: 129.168.0.123

Это тоже не работает, но почему? mod_rewrite определенно устанавливает заголовки, когда правило и условие совпадают (ctx->vary это совокупность ctx->vary_this по всем проверенным условиям):

if (ctx->vary) {
    apr_table_merge(r->headers_out, "Vary", ctx->vary);
}

Это можно проверить с помощью оператора журнала и r->headers_out переменная, используемая при генерации заголовков ответа. Если что-то определенно идет не так, то после выполнения правил могут возникнуть проблемы.

Проблема.htaccess

В настоящее время вы, похоже, определяете свои правила в .htaccess или <Directory> раздел. Это означает, что mod_rewrite работает в фазе исправления Apache, и механизм, который он использует, чтобы фактически выполнить переписывания здесь, очень запутан. Давайте на секунду предположим, что внешнего перенаправления нет, так как у вас была проблема даже без него (а позже я перейду к проблеме с перенаправлением).

После того, как вы выполните перезапись, будет слишком поздно для обработки запроса, чтобы модуль фактически отобразил в файл. Вместо этого он назначает себя обработчиком "содержимого" запроса, и когда запрос достигает этой точки, он выполняет вызов ap_internal_redirect(), Это приводит к созданию нового объекта запроса, который не содержит headers_out стол из оригинала.

При условии, что mod_rewrite не вызывает дальнейших перенаправлений, ответ генерируется из нового объекта запроса, которому никогда не будут назначены соответствующие (оригинальные) заголовки. Можно обойти это, работая в контексте каждого сервера (в основной конфигурации или в <VirtualHost>), но...

Проблема перенаправления

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

Когда Apache получает запрос, через цепочку вызовов функций он ap_process_request(), Это в свою очередь призывает ap_process_request_internal() где происходит большая часть важных шагов анализа запроса (включая вызов mod_rewrite). Он возвращает целочисленный код состояния, который в случае вашего перенаправления устанавливается на 301.

Большинство запросов возвращаются OK (который имеет значение 0), что сразу приводит к ap_finalize_request_protocol(), Однако это не тот случай:

if (access_status == OK) {
    ap_finalize_request_protocol(r);
}
else {
    r->status = HTTP_OK;
    ap_die(access_status, r);
}

ap_die() выполняет некоторые дополнительные манипуляции (например, возвращает код ответа обратно в 301), и в этом конкретном случае заканчивается вызовом ap_send_error_response(),

К счастью, это, наконец, корень проблемы. Хотя может показаться, что это не "задом наперед", и это приводит к разрушению первоначальных заголовков. Есть даже комментарий об этом в источнике:

if (!r->assbackwards) {
    apr_table_t *tmp = r->headers_out;

    /* For all HTTP/1.x responses for which we generate the message,
     * we need to avoid inheriting the "normal status" header fields
     * that may have been set by the request handler before the
     * error or redirect, except for Location on external redirects.
     */
    r->headers_out = r->err_headers_out;
    r->err_headers_out = tmp;
    apr_table_clear(r->err_headers_out);

    if (ap_is_HTTP_REDIRECT(status) || (status == HTTP_CREATED)) {
        if ((location != NULL) && *location) {
            apr_table_setn(r->headers_out, "Location", location);
        }
        //...
    }
//...
}

Обратите внимание, что r->headers_out заменяется, и исходная таблица очищается. Эта таблица содержала всю информацию, которая должна была появиться в ответе, поэтому теперь она потеряна.

Заключение

Если вы не перенаправляете и определяете правила в контексте каждого сервера, кажется, что все работает правильно. Однако это не то, что вы хотите. Я вижу возможный обходной путь, но я не уверен, будет ли он приемлемым, не говоря уже о необходимости перекомпилировать сервер.

Для Vary: Accept-Encoding Я могу только предположить, что он исходит из другого модуля, который ведет себя так, что позволяет заголовку проникать. Я также не уверен, почему у Gumbo не было проблем при его попытке.

Для справки я посмотрел исходный код транка 2.2.14 и 2.2, а также модифицировал и запустил Apache 2.2.15. Похоже, что нет никаких существенных различий между версиями в соответствующих разделах кода.

Вы можете попробовать что-то вроде следующего в качестве обходного пути:

<LocationMatch "^.*lang\=">
    Header onsuccess merge Vary "Accept-Language"
</LocationMatch>

Чтобы специально установить Vary: Accept-LanguageHTTP - заголовок ответа на перенаправлении ответ только (что ожидается здесь), вам нужно установить переменное окружение (например.) В качестве части правила переадресации и использовать это , чтобы установить заголовок условно с директивой.

Вам также необходимо использовать always состояние (в отличие от значения по умолчанию onsuccess) с помощью директивы, чтобы установить это в ответе 3хх (т.е. не-200 ответов).

Например:

      # Redirect requests that have an empty Accept-Language header and "lang=en" is present
RewriteCond %{HTTP:Accept-Language} ^$  
RewriteCond %{QUERY_STRING} ^lang=en  
RewriteRule ^$ /? [E=VARY_ACCEPT_LANGUAGE:1,R=301,L]

# Set/Merge "Vary" header on Accept-Language redirect
Header always merge Vary "Accept-Language" env=VARY_ACCEPT_LANGUAGE

ОДНАКО, заголовок должен быть установлен не только для ответа на перенаправление (когда заголовок пуст), он должен быть установлен для всех ответов на запросы, независимо от того, что Accept-LanguageЗаголовок HTTP-запроса фактически установлен на. Таким образом, полагаться на то, что Apache установит этот заголовок с использованием только перенаправления, в любом случае будет недостаточно (даже если он установил заголовок в ответе, как первоначально ожидалось).

Чтобы установить соответствующий Vary заголовок всех ответов на запросы /?lang=en, включая перенаправление, сделайте это так:

      # Set env var if "/?lang=en" is requested
RewriteCond %{QUERY_STRING} ^lang=en  
RewriteRule ^$ - [E=VARY_ACCEPT_LANGUAGE:1]

# Redirect requests that have an empty Accept-Language header and "lang=en" is present
RewriteCond %{HTTP:Accept-Language} ^$  
RewriteCond %{QUERY_STRING} ^lang=en  
RewriteRule ^$ /? [R=301,L]

# Set/Merge "Vary" header on all responses from "/?lang=en"
Header always merge Vary "Accept-Language" env=VARY_ACCEPT_LANGUAGE

Обратите внимание, однако, что если у вас есть дополнительные внутренние директивы перезаписи, которые заставляют механизм перезаписи запускаться заново, тогда env var VARY_ACCEPT_LANGUAGE переименован в REDIRECT_VARY_ACCEPT_LANGUAGE и выше Headerдиректива не будет успешной. Для этого вам, вероятно, понадобится дополнительная директива. Например:

      Header always merge Vary "Accept-Language" env=REDIRECT_VARY_ACCEPT_LANGUAGE
Другие вопросы по тегам