“没有可接受的变体"来自 Apache 中的多视图 [英] "no acceptable variant" from MultiViews in Apache

查看:19
本文介绍了“没有可接受的变体"来自 Apache 中的多视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

/page/about

...将被处理

/page.php

...PATH_INFO 中可用的请求 URI 的尾随部分.

大多数情况下这可以正常工作,但偶尔会导致诸如

之类的错误

[error] [client 86.x.x.x] 没有可接受的变体:/path/to/document/root/page

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

解决方案

简答

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

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

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

     AddType application/x-httpd-php .php

  • 您客户端的浏览器发送请求接受 标头不包括 */* 作为可接受的 MIME 类型(这是非常不寻常的,这就是您很少看到错误的原因).

  • 你有你的 MultiviewsMatch 指令设置为其默认值 NegotiatedOnly.

您可以通过在 Apache 配置中添加以下咒语来解决错误:

<文件*.php">多视图匹配任何</文件>

说明

了解这里发生的事情至少需要对 Apache 的 mod_negotiation 和 HTTP 的 AcceptAccept-Foo 标头.在遇到 OP 描述的错误之前,我对其中任何一个都一无所知;我启用了 mod_negotiation 不是故意的选择,而是因为 apt-get 就是这样为我设置 Apache 的,而且我在没有太多理解的情况下启用了 MultiViews除了它会让我将 .php 留在我的 URL 末尾之外,还有其他含义.您的情况可能相似或相同.

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

  • AcceptAccept-Language 这样的请求标头让客户端也可以指定他们可以接受的 MIME 类型或语言作为指定可接受类型或语言的加权偏好.(当然,这些只有在服务器具有或能够基于这些标头生成不同响应的情况下才有用.)例如,每当我加载页面时,Chromium 都会为我发送以下标头:

     接受:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8接受编码:gzip,deflate,sdch接受语言:en-GB,en-US;q=0.8,en;q=0.6

  • Apache 的 mod_negotiation 允许您存储多个文件,例如 myresource.html.enmyresource.html.frmyresource.pdf.enmyresource.pdf.fr 位于同一文件夹中,然后自动使用请求的 Accept-* 标头来决定在客户端向 myresource 发送请求.有两种方法可以做到这一点.首先是在同一个文件中创建一个 Type Map 文件明确声明每个可用文档的 MIME 类型和语言的文件夹.另一种是多视图.

  • 启用多视图时...

    <块引用>

    多视图

    <块引用>

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

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

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

<块引用>

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

您可以很容易地测试此行为.只需创建一个名为 test.html 的文件,其中包含字符串Hello World".在启用了 Multiviews 的 Apache 服务器的 webroot 中,然后尝试使用允许 HTML 响应的 Accept 标头来请求它,而不允许的标头.我在本地 (Ubuntu) 机器上使用 curl 演示了这一点:

$ curl --header "Accept: text/html";本地主机/测试你好,世界$ curl --header "Accept: image/png";本地主机/测试<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN><html><head><title>406 Not Acceptable</title></head><body><h1>不可接受</h1><p>在此服务器上找不到所请求资源/test 的适当表示.</p>可用变体:<ul><li><a href=test.html">test.html</a>, 输入 text/html;<小时><address>Apache/2.4.6 (Ubuntu) 服务器在 localhost 端口 80</address></body></html>

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

嗯,默认情况下,答案是 MultiViews 根本不会提供 .php 文件.但是很可能你遵循了互联网上许多帖子中的一个的建议(如果我谷歌 'php apache multiviews'第一名作为这个问题的 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 的情况下...

接受:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

... 以 Firefox 为例:

接受:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

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

如果您检查导致 406 错误的请求的用户代理,您可能会发现它们来自相对不常见的用户代理.当我遇到这个错误时,是当我有一个 <img> 元素的 src 指向一个动态提供图像的 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 用于内容协商的扩展,例如字符集、内容类型、语言或编码.这是最严格的实现,意外的副作用最少,并且是默认行为.

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

<块引用>

您最终可以允许 Any 扩展匹配,即使 mod_mime 无法识别扩展名.

为了限制此规则更改仅适用于 .php 文件,我们使用 指令,像这样:

<文件*.php">多视图匹配任何</文件>

有了那个微小(但难以理解)的变化,我们就完成了!

In one deployment of a PHP-based application, Apache's MultiViews option is being used to hide the .php extension of a request dispatcher script. E.g. a request to

/page/about

...would be handled by

/page.php

...with the trailing part of the request URI available in PATH_INFO.

Most of the time this works fine, but occasionally results in errors like

[error] [client 86.x.x.x] no acceptable variant: /path/to/document/root/page

My question is: What triggers this error occasionally, and how can I fix the problem?

解决方案

Short Answer

This error can occur when all the following are simultaneously true:

  • Your webserver has Multiviews enabled

  • You are allowing Multiviews to serve PHP files by assigning them an arbitrary type with the AddType directive, most likely with a line like this:

      AddType application/x-httpd-php .php
    

  • Your client's browser sends with requests an Accept header that does not include */* as an acceptable MIME type (this is highly unusual, which is why you see the error only rarely).

  • You have your MultiviewsMatch directive set to its default of NegotiatedOnly.

You can resolve the error by adding the following incantation to your Apache config:

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

Explanation

Understanding what is going on here requires getting at least a superficial overview of the workings of Apache's mod_negotiation and HTTP's Accept and Accept-Foo headers. Prior to hitting the bug described by the OP, I knew nothing about either of these; I had mod_negotiation enabled not by deliberate choice but because that's how apt-get set up Apache for me, and I had enabled MultiViews without much understanding of the implications of that besides that it would let me leave .php off the end of my URLs. Your circumstances may be similar or identical.

So here are some important fundamentals that I didn't know:

  • request headers like Accept and Accept-Language let the client specify what MIME types or languages it is acceptable for them to receive the response in, as well as specifying weighted preferences for the acceptable types or languages. (Naturally, these are only useful if the server has, or is capable of generating, different responses based upon these headers.) For example, Chromium sends off the following headers for me whenever I load a page:

      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's mod_negotiation lets you store multiple files like myresource.html.en, myresource.html.fr, myresource.pdf.en and myresource.pdf.fr in the same folder and then automatically use the request's Accept-* headers to decide which to serve when the client sends a request to myresource. There are two ways of doing this. The first is to create a Type Map file in the same folder that explicitly declares the MIME Type and language for each of the available documents. The other is Multiviews.

  • When Multiviews are enabled...

    Multiviews

    ... If the server receives a request for /some/dir/foo and /some/dir/foo does not exist, then the server reads the directory looking for all files named foo.*, and effectively fakes up a type map which names all those files, assigning them the same media types and content-encodings it would have if the client had asked for one of them by name. It then chooses the best match to the client's requirements, and returns that document.

The important thing to note here is that the Accept header is still being respected by Apache even with Multiviews enabled; the only difference from the type map approach is that Apache is inferring the MIME types of files from their file extensions rather than through you explicitly declaring it in a type map.

The no acceptable variant error is thrown (and a 406 response sent) by Apache when there exist files for the URL it has received, but it's not allowed to serve any of them because their MIME types don't match any of the possibilities provided in the request's Accept header. (The same thing can happen if there is, for example, no variant in an acceptable language.) This is compliant with the HTTP spec, which states:

If an Accept header field is present, and if the server cannot send a response which is acceptable according to the combined Accept field value, then the server SHOULD send a 406 (not acceptable) response.

You can test this behaviour easily enough. Just create a file called test.html containing the string "Hello World" in the webroot of an Apache server with Multiviews enabled and then try to request it with an Accept header that permits HTML responses versus one that doesn't. I demonstrate this here on my local (Ubuntu) machine with 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>

This brings us to a question that we haven't yet addressed: how does mod_negotiate determine the MIME type of a PHP file when deciding whether it can serve it? Since the file is going to be executed, and could spit out any Content-Type header it likes, the type isn't known prior to execution.

Well, by default, the answer is that MultiViews simply won't serve .php files. But chances are that you followed the advice of one of the many, many posts on the internet (I get 4 on the first page if I Google 'php apache multiviews', the top one clearly being the one the OP of this question followed, since he actually commented upon it) advocating getting around this using an AddType header, probably looking something like this:

AddType application/x-httpd-php .php

Huh? Why does this magically cause Apache to be happy to serve .php files? Surely browsers aren't including application/x-httpd-php as one of the types they'll accept in their Accept headers?

Well, not exactly. But all the major ones do include */* (thus permitting a response of any MIME type - they're using the Accept header only for expressing preference weighting, not for restricting the types they'll accept.) This causes mod_negotiation to be willing to select and serve .php files as long as some MIME type - any at all! - is associated with them.

For example, if I just type a URL into the address bar in Chromium or Firefox, the Accept header the browser sends is, in the case of Chromium...

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

... and in the case of Firefox:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Both of these headers contain */* as an acceptable content type, and thus permit the server to serve a file of any content type it likes. But some less popular browsers don't accept */* - or perhaps only include it for page requests, not when loading the content of a <script> or <img> tag that you might also be serving through PHP - and that's where our problem comes from.

If you check the user agents of the requests that result in 406 errors, you'll likely see that they're from relatively unusual user agents. When I experienced this error, it was when I had the src of an <img> element pointing to a PHP script that dynamically served images (with the .php extension omitted from the URL), and I first witnessed it failing for BlackBerry users:

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

To get around this, we need to let mod_negotiate serve PHP scripts via some means other than giving them an arbitrary type and then relying upon the browser to send an Accept: */* header. To do this, we use the MultiviewsMatch directive to specify that multiviews can serve PHP files regardless of whether they match the request's Accept header. The default option is NegotiatedOnly:

The NegotiatedOnly option provides that every extension following the base name must correlate to a recognized mod_mime extension for content negotiation, e.g. Charset, Content-Type, Language, or Encoding. This is the strictest implementation with the fewest unexpected side effects, and is the default behavior.

But we can get what we want with the Any option:

You may finally allow Any extensions to match, even if mod_mime doesn't recognize the extension.

To restrict this rule change only to .php files, we use a <Files> directive, like this:

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

And with that tiny (but difficult-to-figure-out) change, we're done!

这篇关于“没有可接受的变体"来自 Apache 中的多视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆