"нет приемлемого варианта" из MultiViews в Apache
В одном развертывании приложения на основе PHP Apache MultiViews
опция используется, чтобы скрыть расширение.php скрипта диспетчера запросов. Например, запрос к
/page/about
... будет обрабатываться
/page.php
... с завершающей частью URI запроса, доступной в PATH_INFO
,
В большинстве случаев это работает нормально, но иногда приводит к таким ошибкам, как
[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page
Мой вопрос: что иногда вызывает эту ошибку, и как я могу исправить проблему?
2 ответа
Короткий ответ
Эта ошибка может возникать, когда все следующее верно одновременно:
- На вашем веб-сервере включена поддержка нескольких просмотров
Вы разрешаете Multiviews обслуживать файлы PHP, присваивая им произвольный тип с помощью
AddType
директива, скорее всего, с такой строкой:AddType application/x-httpd-php .php
- Браузер вашего клиента отправляет с запросами
Accept
заголовок, который не включает*/*
как приемлемый тип MIME (это очень необычно, поэтому вы видите ошибку очень редко). - У вас есть
MultiviewsMatch
директива, установленная по умолчаниюNegotiatedOnly
,
Вы можете устранить ошибку, добавив следующее заклинание в конфигурацию Apache:
<Files "*.php">
MultiviewsMatch Any
</Files>
объяснение
Понимание того, что здесь происходит, требует как минимум поверхностного обзора работы Apache. mod_negotiation
и HTTP Accept
а также Accept-Foo
заголовки. До появления ошибки, описанной в ОП, я ничего не знал ни об одном из них; я имел 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
позволяет хранить несколько файлов, таких как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 из их расширений, а не через явное объявление этого в карте типов.
Apache генерирует ошибку недопустимого варианта (и отправляет ответ 406), когда существуют файлы для 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 на первой странице, если я Google 'php apache multiviews', верхняя из которых, очевидно, является той, за которой следовал 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
директива, указывающая, что множественные просмотры могут обслуживать файлы PHP независимо от того, соответствуют ли они запросу Accept
заголовок. Опция по умолчанию NegotiatedOnly
:
NegotiatedOnly
Опция предусматривает, что каждое расширение, следующее за базовым именем, должно соответствовать распознанномуmod_mime
расширение для согласования контента, например, Charset, Content-Type, Language или Encoding. Это самая строгая реализация с наименьшим количеством неожиданных побочных эффектов, и это поведение по умолчанию.
Но мы можем получить то, что мы хотим с Any
опция:
Вы можете наконец позволить
Any
расширения, чтобы соответствовать, даже еслиmod_mime
не распознает расширение.
Чтобы ограничить это правило, измените только на .php
файлы, мы используем <Files>
директива, вот так:
<Files "*.php">
MultiviewsMatch Any
</Files>
И с этим крошечным (но сложным для понимания) изменением мы закончили!
Ответ, данный Марком Эмери, почти полон, однако в нем не хватает "сладкого места", и он не затрагивает вопрос: "В запросе не указано расширение, поэтому согласование не удается с альтернативами".
Вы можете устранить эту ошибку, добавив следующие config-snippets:
Ваша конфигурация 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
к вашей конфигурации, если эта ошибка беспокоит вас.