10

在基于 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

我的问题是:什么偶尔会触发这个错误,我该如何解决这个问题?

4

2 回答 2

11

简答

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

  • 您的网络服务器启用了多视图

  • AddType您允许 Multiviews 通过使用指令为 PHP 文件分配任意类型来服务 PHP 文件,很可能是这样的一行:

      AddType application/x-httpd-php .php
    
  • 您的客户端浏览器随请求发送一个Accept不包含*/*作为可接受 MIME 类型的标头(这是非常不寻常的,这就是您很少看到错误的原因)。

  • 您将MultiviewsMatch指令设置为其默认值NegotiatedOnly.

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

<Files "*.php">
    MultiviewsMatch Any
</Files>

解释

了解这里发生的事情至少需要对 Apachemod_negotiation和 HTTP 的工作原理AcceptAccept-Foo标头有一个肤浅的概述。在遇到 OP 描述的错误之前,我对这两者都一无所知;我mod_negotiation不是通过刻意选择启用的,而是因为这就是apt-get为我设置 Apache 的方式,而且我启用它时并MultiViews没有太多了解它的含义,除了它会让我离开.phpURL 的末尾。您的情况可能相似或相同。

所以这里有一些我不知道的重要基础知识:

  • 请求标头喜欢AcceptAccept-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
    
  • Apachemod_negotiation允许您将多个文件(如myresource.html.enmyresource.html.fr和)存储在同一个文件夹中,然后自动使用请求的myresource.pdf.en标头来决定当客户端向. 有两种方法可以做到这一点。首先是在同一文件夹中创建一个类型映射文件,该文件明确声明每个可用文档的 MIME 类型和语言。另一个是多视图。myresource.pdf.frAccept-*myresource

  • 启用多视图时...

    多视图

    ...如果服务器收到请求/some/dir/foo/some/dir/foo不存在,则服务器读取目录以查找所有名为 的文件foo.*,并有效地伪造一个类型映射来命名所有这些文件,为它们分配相同的媒体类型和内容编码如果客户按名字要求其中一个,它就会有。然后它选择最符合客户要求的匹配项,并返回该文档。

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

当它收到的 URL 存在文件时,Apache 会抛出不可接受的变体错误(并发送 406 响应),但不允许提供任何文件,因为它们的 MIME 类型与提供的任何可能性都不匹配请求的Accept标头。(例如,如果在可接受的语言中没有变体,也会发生同样的事情。)这符合 HTTP 规范,该规范指出:

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

您可以很容易地测试此行为。只需在启用了 Multiviews 的 Apache 服务器的 webroot 中创建一个test.html包含字符串“Hello World”的文件,然后尝试使用允许 HTML 响应与不允许 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>

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

好吧,默认情况下,答案是 MultiViews 根本不会提供.php文件。但是很可能您遵循了互联网上众多帖子中的一个的建议(如果我 Google '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

这两个标头都包含*/*作为可接受的内容类型,因此允许服务器提供它喜欢的任何内容类型的文件。但是一些不太流行的浏览器接受*/*- 或者可能只在页面请求中包含它,而不是在加载您也可能通过 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+

为了解决这个问题,我们需要让mod_negotiatePHP 脚本通过某种方式提供服务,而不是给它们一个任意类型,然后依靠浏览器发送一个Accept: */*标头。为此,我们使用该MultiviewsMatch指令指定多视图可以服务 PHP 文件,无论它们是否匹配请求的Accept标头。默认选项是NegotiatedOnly

NegotiatedOnly选项规定,基本名称后面的每个扩展都必须与mod_mime内容协商的可识别扩展相关,例如字符集、内容类型、语言或编码。这是最严格的实现,具有最少的意外副作用,并且是默认行为。

但是我们可以通过以下选项得到我们想要的Any

您最终可能会允许Any扩展名匹配,即使mod_mime不识别扩展名。

为了将此规则更改仅限于.php文件,我们使用<Files>指令,如下所示:

<Files "*.php">
    MultiviewsMatch Any
</Files>

有了这个微小(但难以弄清楚)的变化,我们就完成了!

于 2014-07-06T18:12:45.423 回答
4

Mark Amery 给出的答案几乎是完整的,但是它缺少最佳位置,并且没有解决“请求中没有给出扩展名,因此协商失败并有替代方案”的问题。

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

你的 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/*to 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 文件视为 .php 文件application/x-httpd-php,但不幸的是,许多发行版也在 /etc/mime.types 文件中定义了它。因此,可以肯定的是,RemoveType .php如果此错误困扰您,只需将其添加到您的配置中。

于 2014-07-15T13:39:29.817 回答