В одном развертывании приложения на основе PHP параметр MultiViews
Apache используется для спряжения расширения .php сценария диспетчера запросов. Например, запрос
/page/about
… будет обрабатываться
/page.php
… с задней частью URI запроса, доступной в PATH_INFO
.
В большинстве случаев это работает отлично, но иногда приводит к ошибкам, например
[error] [client 86.xxx] no acceptable variant: /path/to/document/root/page
Мой вопрос: что иногда вызывает эту ошибку, и как я могу исправить эту проблему?
Эта ошибка может возникать, когда одновременно выполняются все следующие условия:
Вы разрешаете Multiviews обслуживать файлы PHP, назначая им произвольный тип с директивой AddType
, скорее всего, с такой строкой:
AddType application/x-httpd-php .php
Accept
, который не включает */*
в качестве приемлемого типа MIME (это очень необычно, поэтому вы видите ошибку редко). MultiviewsMatch
установленная по умолчанию NegotiatedOnly
. Вы можете устранить ошибку, добавив следующее заклинание в конфигурацию Apache:
<Files "*.php"> MultiviewsMatch Any </Files>
Понимание того, что здесь происходит, требует, по крайней мере, поверхностного обзора работы mod_negotiation
от Apache и mod_negotiation
Accept
и Accept-Foo
HTTP. До того, как я столкнулся с ошибкой, описанной OP, я ничего не знал об этом; У меня mod_negotiation
включен не по преднамеренному выбору, а потому, что именно так apt-get
настроил Apache для меня, и я включил MultiViews
без особого понимания последствий этого, кроме того, что он позволил бы мне оставить .php
с конца моих URL-адресов. Ваши обстоятельства могут быть похожими или идентичными.
Итак, вот некоторые важные основы, которые я не знал:
заголовки запросов, такие как Accept
и Accept-Language
позволяют клиенту указать, какие типы или языки MIME приемлемы для них, чтобы получать ответ, а также задавать взвешенные предпочтения для приемлемых типов или языков. (Естественно, они полезны только в том случае, если сервер имеет или способен генерировать разные ответы на основе этих заголовков.) Например, Chromium отправляет мне следующие заголовки всякий раз, когда я загружаю страницу:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-GB,en-US;q=0.8,en;q=0.6
mod_negotiation
Apache позволяет хранить несколько файлов, таких как myresource.html.en
, myresource.html.fr
, myresource.pdf.en
и myresource.pdf.fr
в той же папке, а затем автоматически использовать заголовки Accept-*
запроса, чтобы решить, когда клиент отправляет запрос на myresource
. Есть два способа сделать это. Первый заключается в создании файла карты типов в той же папке, которая явно объявляет тип и язык MIME для каждого из доступных документов. Другой – Multiviews.
Когда разрешены многопользовательские …
MultiViews
… Если сервер получает запрос для
/some/dir/foo
и/some/dir/foo
не существует, тогда сервер читает каталог, ищущий все файлы с именемfoo.*
, И эффективно подделывает карту типа, которая называет все эти файлы, назначая им те же типы носителей и кодировки содержимого, которые он имел бы, если бы клиент попросил одного из них по имени. Затем он выбирает наилучшее соответствие требованиям клиента и возвращает этот документ.
Важно отметить, что заголовок Accept
по-прежнему соблюдается Apache даже при включенном режиме Multiviews; единственное отличие от подхода типа карты заключается в том, что Apache выводит типы файлов MIME из своих расширений файлов, а не через то, что вы явно объявляете его на карте типов.
Ошибки приемлемого варианта не выбрасываются (и отправлено 406 ответов) Apache, когда существуют файлы для полученного им URL-адреса, но не разрешено обслуживать ни одно из них, потому что их типы MIME не соответствуют ни одной из возможностей, предоставляемых в заголовок Accept
. (То же самое может случиться, если на приемлемом языке нет варианта). Это соответствует спецификации HTTP, которая гласит:
Если присутствует поле заголовка Accept, и если сервер не может отправить ответ, который является приемлемым в соответствии с комбинированным значением поля Accept, тогда сервер ДОЛЖЕН отправить 406 (неприемлемый) ответ.
Вы можете проверить это поведение достаточно легко. Просто создайте файл с именем test.html
содержащий строку «Hello World» в webroot сервера Apache с включенными Multiviews, а затем попробуйте запросить его с заголовком Accept, который разрешает ответы HTML по сравнению с тем, который этого не делает. Я демонстрирую это здесь на своей локальной машине (Ubuntu) с curl
:
$ curl --header "Accept: text/html" localhost/test Hello World $ curl --header "Accept: image/png" localhost/test <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate representation of the requested resource /test could not be found on this server.</p> Available variants: <ul> <li><a href="test.html">test.html</a> , type text/html</li> </ul> <hr> <address>Apache/2.4.6 (Ubuntu) Server at localhost Port 80</address> </body></html>
Это приводит нас к вопросу, который мы еще не рассмотрели: как mod_negotiate
определяет тип MIME файла PHP при принятии решения о том, может ли он служить ему? Поскольку файл будет выполнен и может выплеснуть любой заголовок Content-Type
ему нравится, этот тип неизвестен до исполнения.
Ну, по умолчанию, ответ заключается в том, что MultiViews просто не будет служить .php
файлам. Но есть вероятность, что вы последовали за советом одной из многих, много сообщений в Интернете (я получаю 4 на первой странице, если я использую «php apache multiviews» в Google, верхняя часть которой, очевидно, является той, которую придерживался OP этого вопроса, поскольку он фактически прокомментировал это), защищая это, используя заголовок AddType, вероятно, выглядя примерно так:
AddType application/x-httpd-php .php
А? Почему это волшебным образом заставляет Apache быть счастливым служить .php
файлами? Разумеется, браузеры не включают application/x-httpd-php
качестве одного из типов, которые они будут принимать в заголовках Accept
?
Ну, не совсем. Но все основные из них включают */*
(таким образом, разрешая ответ любого типа MIME – они используют заголовок Accept
только для выражения предпочтительного взвешивания, а не для ограничения типов, которые они будут принимать). Это вызывает mod_negotiation
выбирать и обслуживать файлы .php
пока какой-то тип MIME – вообще! – связано с ними.
Например, если я просто набираю URL-адрес в адресной строке в Chromium или Firefox, заголовок Accept
отправляемый браузером, в случае Chromium …
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
… и в случае Firefox:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Оба этих заголовка содержат */*
как приемлемый тип контента и, таким образом, позволяют серверу обслуживать файл любого типа контента, который ему нравится. Но некоторые менее популярные браузеры не принимают */*
– или могут включать только его для запросов страниц, а не при загрузке содержимого <script>
или <img>
который вы также можете использовать через PHP, – и это то, где наши проблема возникает.
Если вы проверите пользовательские агенты запросов, которые приводят к 406 ошибкам, вы, вероятно, увидите, что они из относительно необычных пользовательских агентов. Когда я столкнулся с этой ошибкой, это было, когда у меня был src
элемента <img>
указывающий на PHP-скрипт, который динамически обслуживал образы (с расширением .php
пропущенным из URL-адреса), и я впервые увидел, что он не работает для пользователей BlackBerry:
Mozilla/5.0 (BlackBerry; U; BlackBerry 9320; fr) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.714 Mobile Safari/534.11+
Чтобы обойти это, мы должны позволить mod_negotiate
обслуживать PHP-скрипты с помощью каких-то иных средств, кроме предоставления им произвольного типа, а затем полагаться на браузер для отправки заголовка Accept: */*
. Для этого мы используем директиву MultiviewsMatch
чтобы указать, что multiviews могут обслуживать файлы PHP независимо от того, соответствуют ли они заголовку Accept
. По умолчанию используется NegotiatedOnly
:
Параметр
NegotiatedOnly
предусматривает, что каждое расширение, следующее за базовым именем, должно соотноситься с признанным расширениемmod_mime
для согласования контента, например Charset, Content-Type, Language или Encoding. Это самая строгая реализация с наименьшими неожиданными побочными эффектами, и это поведение по умолчанию.
Но мы можем получить то, что хотим, с опцией Any
:
Вы можете, наконец, разрешить
Any
расширения, даже еслиmod_mime
не распознает расширение.
Чтобы ограничить это правило изменением только на .php
файлы, мы используем директиву <Files>
, например:
<Files "*.php"> MultiviewsMatch Any </Files>
И с этой крошечной (но труднодоступной) изменой мы закончили!
Ответ, данный Марк Эмери, почти завершен, однако ему не хватает сладкого пятна и не обращается к «никакому расширению, указанному в запросе, поэтому переговоры не сменяются альтернативами.
Вы можете устранить эту ошибку, добавив следующие параметры:
Ваша конфигурация PHP должна быть примерно такой:
<FilesMatch "\.ph(p3?|tml)$"> SetHandler application/x-httpd-php </FilesMatch>
НЕ используйте AddType application/x-httpd-php .php
или любой другой AddType
И ваша дополнительная конфигурация должна быть такой:
RemoveType .php <Files "*.php"> MultiviewsMatch Any </Files>
Если вы используете AddType, вы получите такие ошибки:
GET /index/123/434 HTTP/1.1 Host: test.net Accept: image/* HTTP/1.1 406 Not Acceptable Date: Tue, 15 Jul 2014 13:08:27 GMT Server: Apache Alternates: {"index.php" 1 {type application/x-httpd-php}} Vary: Accept-Encoding Content-Length: 427 Connection: close Content-Type: text/html; charset=iso-8859-1
Как вы можете видеть, он находит index.php, однако он не использует эту альтернативу, поскольку не может сопоставлять Accept: image/*
с application/x-httpd-php
. Если вы запрашиваете /index.php/1/2/3/4
он работает нормально.
Причина этого я нашел в исходном коде модуля mod_negotiation. Я пытался выяснить, почему Apache будет работать, если тип .php был «cgi», но не иначе (подсказка: application/x-httpd-cgi
жестко закодирована ..). Хотя в источнике я заметил, что apache будет видеть только файл в качестве соответствия, если Content-Type этого файла соответствует заголовку Accept или если Content-Type этого файла пуст.
Если вы используете SetHandler, то apache не увидит файлы .php как application/x-httpd-php
, но, к сожалению, многие дистрибутивы также определяют это в файле /etc/mime.types. Так что, конечно, просто добавьте RemoveType .php
в свою конфигурацию, если эта ошибка вас беспокоит.
Насколько я понимаю MultiViews, когда вы запрашиваете «/ page / about», если родительский каталог принадлежит «page.php», MultiViews включен, apache будет искать подкаталог с именем «страница». Если он не может найти его, он будет искать файлы с именем «page. *» И будет запускать их в соответствии с файлами / типами носителей, установленными в конфигурациях apache, то есть, если «page.php» был найден, он вернет page.php и если «page.html» был найден, он запустит это.
Если apache не сталкивается с каталогом с именем «страница» или с файлом с именем «страница. *», Он выдает ошибку. Чтобы предотвратить его, убедитесь, что у вас есть файл или каталог, на который вы ссылаетесь. Я предпочитаю использовать apache mod_rewrite и направлять все запросы в один файл и обрабатывать его там.