来自Apache的MultiViews的"无可接受的变体"

cri*_*hoj 10 php apache multiviews

在基于PHP的应用程序的一个部署中,Apache的MultiViews选项用于隐藏请求调度程序脚本的.php扩展名.例如请求

/page/about
Run Code Online (Sandbox Code Playgroud)

......将由...处理

/page.php
Run Code Online (Sandbox Code Playgroud)

...有可用的请求URI的尾部PATH_INFO.

大多数时候这种方法很好,但偶尔会导致错误

[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page
Run Code Online (Sandbox Code Playgroud)

我的问题是:偶尔触发此错误的原因是什么,以及如何解决问题?

Mar*_*ery 10

简答

当以下所有内容同时为真时,可能会发生此错误:

  • 您的网络服务器已启用多视图
  • 您允许Multiviews通过为AddType指令指定任意类型来提供PHP文件,最有可能使用如下行:

    AddType application/x-httpd-php .php
    
    Run Code Online (Sandbox Code Playgroud)
  • 您的客户端的浏览器向请求发送一个Accept不包含*/*在可接受的MIME类型中的标头(这是非常不寻常的,这就是您很少看到错误的原因).
  • 您的MultiviewsMatch指令设置为其默认值NegotiatedOnly.

您可以通过将以下咒语添加到Apache配置来解决该错误:

<Files "*.php">
    MultiviewsMatch Any
</Files>
Run Code Online (Sandbox Code Playgroud)

说明

了解这里发生的事情需要至少对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
    
    Run Code Online (Sandbox Code Playgroud)
  • Apache的mod_negotiation,您可以存储多个文件一样myresource.html.en,myresource.html.fr,myresource.pdf.enmyresource.pdf.fr在同一个文件夹中,然后自动使用请求的Accept-*标题来决定当客户端发送请求到服务myresource.有两种方法可以做到这一点.第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言.另一个是Multiviews.

  • 启用多视图时...

    在Multiviews

    ...如果服务器收到请求/some/dir/foo/some/dir/foo不存在,则服务器会寻找一个名为所有文件的目录foo.*,并有效假货一个类型的地图,所有的名字这些文件,赋予它们不同的介质类型和内容编码如果客户要求其中一个名字,它会有.然后,它会根据客户的要求选择最佳匹配,并返回该文档.

这里要注意的重要一点是,Accept即使启用了Multiview,Apache仍然会尊重标题; 与类型映射方法的唯一区别是Apache正在从文件扩展名中推断文件的MIME类型,而不是通过在类型映射中明确声明它.

没有可接受的变异引发错误(和406响应发送)由阿帕奇当存在文件已收到的网址,但它不能成为任何人,因为他们的MIME类型不匹配,任何在规定的可能性请求的Accept标题.(例如,如果没有可接受语言的变体,则会发生同样的情况.)这符合HTTP规范,其中规定:

如果存在Accept头字段,并且如果服务器无法根据组合的Accept字段值发送可接受的响应,则服务器应该发送406(不可接受)响应.

您可以轻松地测试此行为.只需test.html在启用了Multiviews的Apache服务器的webroot中创建一个名为包含字符串"Hello World" 的文件,然后尝试使用允许HTML响应的Accept标头与不响应的标头请求它.我在我的本地(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>
Run Code Online (Sandbox Code Playgroud)

这给我们带来了一个我们尚未解决的问题:mod_negotiate在决定是否可以为PHP文件提供服务时,如何确定PHP文件的MIME类型?由于文件将被执行,并且可能会吐出Content-Type它喜欢的任何标头,因此在执行之前不知道该类型.

嗯,默认情况下,答案是MultiViews根本不会提供.php文件.但是很可能你是按照互联网上很多很多帖子之一的建议(如果我谷歌的'php apache multiviews',我在第一页上得到4 ,明显的是这个问题的OP跟随的那个,因为他实际上评论过它)主张使用AddType标头绕过它,可能看起来像这样:

AddType application/x-httpd-php .php
Run Code Online (Sandbox Code Playgroud)

咦?为什么这会让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
Run Code Online (Sandbox Code Playgroud)

......以及Firefox的情况:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Run Code Online (Sandbox Code Playgroud)

这两个标头都包含*/*可接受的内容类型,因此允许服务器提供其喜欢的任何内容类型的文件.但是一些不太受欢迎的浏览器接受*/*- 或者可能只包含它用于页面请求,而不是在加载您可能也通过PHP提供的标记<script><img>标记的内容时- 这就是我们的问题所在.

如果检查导致406错误的请求的用户代理,您可能会发现它们来自相对不寻常的用户代理.当我遇到这个错误时,就是当我有src一个<img>指向动态提供图像的PHP脚本的元素时(.phpURL中省略了扩展名),我首先目睹了BlackBerry用户的失败:

Mozilla/5.0 (BlackBerry; U; BlackBerry 9320; fr) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.714 Mobile Safari/534.11+
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,我们需要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>
Run Code Online (Sandbox Code Playgroud)

随着这一微小(但难以弄清楚)的变化,我们已经完成了!


小智 5

马克·阿默里给出的答案几乎是完整的,但是它缺少最佳点,并且没有解决“请求中没有给出延期,因此与替代方案的谈判失败”的问题。

您可以通过添加以下配置片段来解决此错误:

你的 PHP 配置应该是这样的:

<FilesMatch "\.ph(p3?|tml)$">
    SetHandler application/x-httpd-php
</FilesMatch>
Run Code Online (Sandbox Code Playgroud)

不要使用AddType application/x-httpd-php .php或任何其他 AddType

你的附加配置应该是这样的:

RemoveType .php
<Files "*.php">
    MultiviewsMatch Any
</Files>
Run Code Online (Sandbox Code Playgroud)

如果您确实使用 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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,它确实找到了index.php,但是它没有使用此替代方案,因为它无法匹配Accept: image/*to application/x-httpd-php。如果你要求/index.php/1/2/3/4它工作正常。

我在mod_negotiation模块的源代码中找到了这个原因。我试图找出为什么 Apache 在 .php 类型是“cgi”的情况下可以工作,但在其他情况下就不行(提示:application/x-httpd-cgi是硬编码的..)。在源代码中,我注意到,如果该文件的 Content-Type 与 Accept 标头匹配,或者该文件的 Content-Type 为空,则 apache 只会将该文件视为匹配项。

如果您使用 SetHandler,那么 apache 不会将 .php 文件视为application/x-httpd-php,但不幸的是,许多发行版也在 /etc/mime.types 文件中定义了这一点。因此,可以肯定的是,RemoveType .php如果此错误困扰您,只需将其添加到您的配置中即可。