«Нет приемлемого варианта» из MultiViews в Apache

В одном развертывании приложения на основе PHP параметр MultiViews Apache используется для спряжения расширения .php сценария диспетчера запросов. Например, запрос

 /page/about 

… будет обрабатываться

 /page.php 

… с задней частью URI запроса, доступной в PATH_INFO .

В большинстве случаев это работает отлично, но иногда приводит к ошибкам, например

 [error] [client 86.xxx] no acceptable variant: /path/to/document/root/page 

Мой вопрос: что иногда вызывает эту ошибку, и как я могу исправить эту проблему?

Короткий ответ

Эта ошибка может возникать, когда одновременно выполняются все следующие условия:

  • На вашем веб-сервере включен Multiviews
  • Вы разрешаете 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 и направлять все запросы в один файл и обрабатывать его там.