在基于PHP的应用程序的一个部署中,Apache的MultiViews
选项用于隐藏请求调度程序脚本的.php扩展名。例如。
/page/about
......将由
处理/page.php
... PATH_INFO
中提供了请求URI的尾部。
大多数时候这种方法很好,但偶尔会导致像
这样的错误[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page
我的问题是:偶尔触发此错误的原因是什么,以及如何解决问题?
答案 0 :(得分:9)
如果同时满足以下所有条件,则会发生此错误:
您允许Multiviews通过使用AddType
指令为其分配任意类型来提供PHP文件,很可能使用如下所示的行:
AddType application/x-httpd-php .php
Accept
标头,其中不包含*/*
作为可接受的MIME类型(这非常不寻常,这就是您很少看到错误的原因) MultiviewsMatch
指令设置为默认值NegotiatedOnly
。您可以通过将以下咒语添加到Apache配置来解决错误:
<Files "*.php">
MultiviewsMatch Any
</Files>
了解这里发生的事情需要至少对Apache的mod_negotiation
和HTTP Accept
和Accept-Foo
标题的工作情况进行表面的概述。在遇到OP描述的错误之前,我对这两者中的任何一个都一无所知;我mod_negotiation
启用了apt-get
而不是故意选择,但因为MultiViews
为我设置了Apache,我启用了.php
而不了解其中的含义除此之外它会让我离开我的网址末尾Accept
。您的情况可能相似或相同。
所以这里有一些我不知道的重要基础知识:
请求标头Accept-Language
和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
让客户端指定接收响应的MIME类型或语言,以及指定可接受类型的加权首选项或语言。 (当然,这些仅在服务器具有或能够基于这些标头生成不同响应时才有用。)例如,每当我加载页面时,Chromium都会为我发送以下标题:
mod_negotiation
Apache的myresource.html.en
允许您将多个文件存储在同一文件夹中,例如myresource.html.fr
,myresource.pdf.en
,myresource.pdf.fr
和Accept-*
然后,当客户端向myresource
发送请求时,会自动使用请求的/some/dir/foo
标头来决定投放哪个标头。有两种方法可以做到这一点。第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言。另一个是Multiviews。
启用多视图时...
在Multiviews
...如果服务器收到
/some/dir/foo
的请求且foo.*
不存在,则服务器会读取目录,查找名为Accept
的所有文件,并有效地伪造一个键入所有这些文件的地图,为它们分配相同的媒体类型和内容编码,如果客户端通过名称请求其中一个文件。然后,它会根据客户的要求选择最佳匹配,并返回该文档。
这里需要注意的重要一点是,即使启用了Multiview,Apache仍然会尊重Accept
标题。与类型映射方法的唯一区别是Apache从文件扩展名中推断出文件的MIME类型,而不是通过在类型映射中明确声明它。
如果存在已收到的URL文件,则会抛出无可接受的变种错误(并发送406响应),但不允许其提供任何服务因为他们的MIME类型与请求的test.html
标头中提供的任何可能性都不匹配。 (例如,如果没有可接受语言的变体,也会发生同样的事情。)这符合HTTP规范,其中规定:
如果存在Accept头字段,并且服务器无法根据组合的Accept字段值发送可接受的响应,则服务器应该发送406(不可接受)响应。
您可以轻松地测试此行为。只需创建一个名为curl
的文件,其中包含字符串&#34; Hello World&#34;在启用了Multiview的Apache服务器的webroot中,然后尝试使用允许HTML响应的Accept标头与不响应的标头请求它。我在我的本地(Ubuntu)机器上用$ 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
这给我们带来了一个我们尚未解决的问题:Content-Type
在决定是否可以提供PHP文件时,如何确定PHP文件的MIME类型?由于文件将被执行,并且可以吐出它喜欢的任何.php
标题,因此在执行之前不知道该类型。
嗯,默认情况下,答案是MultiViews只是不会提供AddType application/x-httpd-php .php
个文件。但有可能你是按照互联网上很多很多帖子之一的建议(如果我是谷歌'php apache multiviews',我在第一页上有4个,top one显然是这个的OP问题随之而来,因为他实际上评论过它)主张使用AddType标题解决这个问题,可能看起来像这样:
.php
咦?为什么这会让Apache非常乐意提供application/x-httpd-php
个文件?当然浏览器不包括Accept
作为他们*/*
标题中接受的类型之一?
嗯,不完全是。但所有主要版本都包含Accept
(因此允许任何MIME类型的响应 - 他们仅使用mod_negotiation
标头表示偏好权重,不限制他们接受的类型。)这会导致.php
愿意选择并提供Accept
文件,只要有一些MIME类型 - 任何一个! - 与他们有关。
例如,如果我只是在Chromium或Firefox的地址栏中键入一个网址,那么浏览器发送的Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
标题就是Chromium ...
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
......以及Firefox的情况:
*/*
这两个标头都包含*/*
作为可接受的内容类型,因此允许服务器提供其喜欢的任何内容类型的文件。但是一些不那么流行的浏览器不接受<script>
- 或者可能只包含它用于页面请求,而不是在加载<img>
或{{1}的内容时你也可能通过PHP提供的标签 - 以及我们的问题来自哪里。
如果您检查导致406错误的请求的用户代理,您可能会发现他们来自相对不寻常的用户代理。当我遇到此错误时,就是当src
<img>
个.php
元素指向动态提供图像的PHP脚本时(URL中省略了Mozilla/5.0 (BlackBerry; U; BlackBerry 9320; fr) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.714 Mobile Safari/534.11+
扩展名),以及我第一次目睹BlackBerry用户失败了:
mod_negotiate
为了解决这个问题,我们需要让Accept: */*
通过一些方式提供PHP脚本,而不是给它们任意类型,然后依靠浏览器发送MultiviewsMatch
标题。为此,我们使用Accept
指令指定多视图可以为PHP文件提供服务,无论它们是否与请求的NegotiatedOnly
标头匹配。默认选项为NegotiatedOnly
:
Any
选项规定,基本名称后面的每个扩展名都必须与用于内容协商的已识别mod_mime
扩展程序相关联,例如:字符集,内容类型,语言或编码。这是最严格的实现,具有最少的意外副作用,并且是默认行为。
但我们可以使用Any
选项获得我们想要的内容:
即使
mod_mime
无法识别扩展程序,您最终也可以允许.php
个扩展程序匹配。
要将此规则更改为仅限<Files "*.php">
MultiviewsMatch Any
</Files>
个文件,我们使用<Files>
指令,如下所示:
{{1}}
随着这一微小(但难以弄清楚)的改变,我们已经完成了!
答案 1 :(得分:3)
马克·阿梅里给出的答案几乎已经完成,但它缺少最佳位置,并没有解决“请求中没有给出延期,因此协商失败的替代方案。”
您可以通过添加以下配置片段来解决此错误:
你的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模块的源代码中找到了这个的原因。我试图找出为什么如果.php类型是'cgi'而Apache不会工作的原因(提示:application/x-httpd-cgi
是硬编码的......)。在源代码中,我注意到如果该文件的Content-Type与Accept标头匹配,或者该文件的Content-Type为空,则apache只会将该文件视为匹配。
如果使用SetHandler而不是apache,则不会将.php文件看作application/x-httpd-php
,但不幸的是,许多发行版也在/etc/mime.types文件中定义了这个。所以可以肯定的是,如果这个错误困扰你,只需将RemoveType .php
添加到你的配置中。