[英]“no acceptable variant” from MultiViews in Apache
在基于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
我的问题是:偶尔会触发此错误的原因是什么,以及如何解决问题?
当以下所有内容同时为真时,可能会发生此错误:
您允许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
为我设置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
Apache的mod_negotiation
允许您将多个文件(如myresource.html.en
, myresource.html.fr
, myresource.pdf.en
和myresource.pdf.fr
在同一文件夹中,然后自动使用请求的Accept-*
标myresource.pdf.fr
决定要提供哪些文件当客户端向myresource
发送请求时。 有两种方法可以做到这一点。 第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言。 另一个是Multiviews。
启用多视图时...
在Multiviews
...如果服务器收到
/some/dir/foo
的请求和/some/dir/foo
不存在,那么服务器会读取名为foo.*
所有文件的目录,并有效地伪造一个类型映射命名所有这些文件,为它们分配相同的媒体类型和内容编码,如果客户端通过名称请求其中一个文件。 然后,它会根据客户的要求选择最佳匹配,并返回该文档。
这里需要注意的重要一点是,即使启用了Multiviews,Apache仍然会遵守Accept
标头; 与类型映射方法的唯一区别是Apache正在从文件扩展名中推断文件的MIME类型,而不是通过在类型映射中明确声明它。
当存在已收到的URL的文件时,Apache会抛出不可接受的变量错误(并发送406响应),但不允许它们提供任何服务,因为它们的MIME类型与提供的任何可能性不匹配。请求的Accept
标头。 (例如,如果没有可接受语言的变体,则会发生同样的情况。)这符合HTTP规范,其中规定:
如果存在Accept头字段,并且如果服务器无法根据组合的Accept字段值发送可接受的响应,则服务器应该发送406(不可接受)响应。
您可以轻松地测试此行为。 只需在启用了Multiview的Apache服务器的webroot中创建一个名为test.html
的文件,其中包含字符串“Hello World”,然后尝试使用允许HTML响应的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
在决定是否可以提供PHP文件时,如何确定PHP文件的MIME类型? 由于文件将被执行,并且可能会吐出它喜欢的任何Content-Type
标头,因此在执行之前不知道该类型。
好吧,默认情况下,答案是MultiViews根本不会提供.php
文件。 但是很可能你是按照互联网上很多很多帖子之一的建议(如果我谷歌'php apache multiviews' ,我在第一页上得到4, 最明显的是这个问题的OP跟随的那个,因为他实际上评论过它)主张使用AddType标头绕过它,可能看起来像这样:
AddType application/x-httpd-php .php
咦? 为什么这会让Apache非常乐意为.php
文件服务呢? 当然浏览器不包括application/x-httpd-php
作为他们在Accept
头中接受的类型之一?
好吧,不完全是。 但是所有主要的都包含*/*
(因此允许任何MIME类型的响应 - 他们只使用Accept
标头表示偏好权重, 而不是限制他们接受的类型。)这导致mod_negotiation
愿意选择并提供.php
文件,只要有一些MIME类型 - 任何一个! - 与他们相关联。
例如,如果我只是在Chromium或Firefox的地址栏中输入一个URL,那么浏览器发送的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错误的请求的用户代理,您可能会发现它们来自相对不寻常的用户代理。 当我遇到这个错误时,就是当我的<img>
元素的src
指向一个动态提供图像的PHP脚本(从URL中省略了.php
扩展名)时,我首先目睹了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
指令指定多MultiviewsMatch
可以为PHP文件提供服务,无论它们是否与请求的Accept
标头匹配。 默认选项是NegotiatedOnly
:
NegotiatedOnly
选项规定,基本名称后面的每个扩展名必须与用于内容协商的已识别mod_mime
扩展名相关联,例如Charset,Content-Type,Language或Encoding。 这是最严格的实现,具有最少的意外副作用,并且是默认行为。
但是我们可以使用Any
选项获得我们想要的东西:
即使
mod_mime
无法识别扩展名,您也可以最终允许Any
扩展名匹配。
要限制此规则仅更改为.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模块的源代码中找到了这个的原因。 我试图找出为什么如果.php类型是'cgi'而Apache不会工作的原因(提示: application/x-httpd-cgi
是硬编码的......)。 在源代码中,我注意到如果该文件的Content-Type与Accept标头匹配,或者该文件的Content-Type为空,则apache只会看到该文件为匹配项。
如果使用SetHandler而不是apache,则不会将.php文件看作application/x-httpd-php
,但不幸的是,许多发行版也在/etc/mime.types文件中定义了这个。 所以可以肯定的是,如果这个bug困扰你,只需将RemoveType .php
添加到你的配置中。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.