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-Language
HTTP - заголовок ответа на перенаправлении ответ только (что ожидается здесь), вам нужно установить переменное окружение (например.) В качестве части правила переадресации и использовать это , чтобы установить заголовок условно с директивой.
Вам также необходимо использовать
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