"нет приемлемого варианта" из 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 к вашей конфигурации, если эта ошибка беспокоит вас.

Другие вопросы по тегам