简体   繁体   English

来自Apache的MultiViews的“无可接受的变体”

[英]“no acceptable variant” from MultiViews in Apache

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. 在基于PHP的应用程序的一个部署中,Apache的MultiViews选项用于隐藏请求调度程序脚本的.php扩展名。 Eg a request to 例如请求

/page/about

...would be handled by ......将由...处理

/page.php

...with the trailing part of the request URI available in PATH_INFO . ...使用PATH_INFO可用的请求URI的尾部。

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: 您允许Multiviews通过使用AddType指令为它们分配任意类型来提供PHP文件,很可能使用如下所示的行:

     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). 您的客户端浏览器向请求发送一个Accept标头,该标头不包含*/*作为可接受的MIME类型(这非常不寻常,这就是您很少看到错误的原因)。
  • You have your MultiviewsMatch directive set to its default of NegotiatedOnly . 您将MultiviewsMatch指令设置为其默认值NegotiatedOnly

You can resolve the error by adding the following incantation to your Apache config: 您可以通过将以下咒语添加到Apache配置来解决该错误:

<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. 了解这里发生的事情需要至少对Apache的mod_negotiation和HTTP的AcceptAccept-Foo标头的工作情况进行表面的概述。 Prior to hitting the bug described by the OP, I knew nothing about either of these; 在遇到OP描述的错误之前,我对这两者中的任何一个都一无所知; 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. 我没有通过刻意的选择启用mod_negotiation ,但是因为这就是apt-get为我设置Apache的方式,并且我已经启用了MultiViews而没有太多理解它的含义,除了它会让我离开.php离开我的URL的末尾。 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. 请求标头(如AcceptAccept-Language允许客户端指定接收响应的MIME类型或语言,以及为可接受的类型或语言指定加权首选项。 (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: (当然,这些仅在服务器具有或能够根据这些标头生成不同响应时才有用。)例如,每当我加载页面时,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'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 . Apache的mod_negotiation允许您将多个文件(如myresource.html.enmyresource.html.frmyresource.pdf.enmyresource.pdf.fr在同一文件夹中,然后自动使用请求的Accept-*myresource.pdf.fr决定要提供哪些文件当客户端向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. 第一种是在同一文件夹中创建一个Type Map文件,该文件显式声明每个可用文档的MIME类型和语言。 The other is Multiviews. 另一个是Multiviews。

  • When Multiviews are enabled... 启用多视图时...

    Multiviews 在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. ...如果服务器收到/some/dir/foo的请求和/some/dir/foo不存在,那么服务器会读取名为foo.*所有文件的目录,并有效地伪造一个类型映射命名所有这些文件,为它们分配相同的媒体类型和内容编码,如果客户端通过名称请求其中一个文件。 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; 这里需要注意的重要一点是,即使启用了Multiviews,Apache仍然会遵守Accept标头; 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. 与类型映射方法的唯一区别是Apache正在从文件扩展名中推断文件的MIME类型,而不是通过在类型映射中明确声明它。

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. 当存在已收到的URL的文件时,Apache会抛出不可接受的变量错误(并发送406响应),但不允许它们提供任何服务,因为它们的MIME类型与提供的任何可能性不匹配。请求的Accept标头。 (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: (例如,如果没有可接受语言的变体,则会发生同样的情况。)这符合HTTP规范,其中规定:

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

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. 只需在启用了Multiview的Apache服务器的webroot中创建一个名为test.html的文件,其中包含字符串“Hello World”,然后尝试使用允许HTML响应的Accept标头与不响应HTML响应的标头请求它。 I demonstrate this here on my local (Ubuntu) machine with curl : 我在我的本地(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>

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

Well, by default, the answer is that MultiViews simply won't serve .php files. 好吧,默认情况下,答案是MultiViews根本不会提供.php文件。 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: 但是很可能你是按照互联网上很多很多帖子之一的建议(如果我谷歌'php apache multiviews' ,我在第一页上得到4, 明显的是这个问题的OP跟随的那个,因为他实际上评论过它)主张使用AddType标头绕过它,可能看起来像这样:

AddType application/x-httpd-php .php

Huh? 咦? Why does this magically cause Apache to be happy to serve .php files? 为什么这会让Apache非常乐意为.php文件服务呢? Surely browsers aren't including application/x-httpd-php as one of the types they'll accept in their Accept headers? 当然浏览器不包括application/x-httpd-php作为他们在Accept头中接受的类型之一?

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! 但是所有主要的都包含*/* (因此允许任何MIME类型的响应 - 他们只使用Accept标头表示偏好权重, 而不是限制他们接受的类型。)这导致mod_negotiation愿意选择并提供.php文件,只要有一些MIME类型 - 任何一个! - 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... 例如,如果我只是在Chromium或Firefox的地址栏中输入一个URL,那么浏览器发送的Accept标头就是Chromium ...

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

... and in the case of Firefox: ......以及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. 但是一些不那么流行的浏览器接受*/* - 或者可能只包含它用于页面请求,而不是在加载<script><img>标签的内容时,你也可能通过PHP提供 - 而这就是我们的问题来自。

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. 如果检查导致406错误的请求的用户代理,您可能会发现它们来自相对不寻常的用户代理。 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: 当我遇到这个错误时,就是当我的<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+

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. 为了解决这个问题,我们需要让mod_negotiate通过某些方式提供PHP脚本,而不是给它们任意类型,然后依靠浏览器发送Accept: */*标题。 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. 为此,我们使用MultiviewsMatch指令指定多MultiviewsMatch可以为PHP文件提供服务,无论它们是否与请求的Accept标头匹配。 The default option is NegotiatedOnly : 默认选项是NegotiatedOnly

The NegotiatedOnly option provides that every extension following the base name must correlate to a recognized mod_mime extension for content negotiation, eg Charset, Content-Type, Language, or Encoding. NegotiatedOnly选项规定,基本名称后面的每个扩展名必须与用于内容协商的已识别mod_mime扩展名相关联,例如Charset,Content-Type,Language或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: 但是我们可以使用Any选项获得我们想要的东西:

You may finally allow Any extensions to match, even if mod_mime doesn't recognize the extension. 即使mod_mime无法识别扩展名,您也可以最终允许Any扩展名匹配。

To restrict this rule change only to .php files, we use a <Files> directive, like this: 要限制此规则仅更改为.php文件,我们使用<Files>指令,如下所示:

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

And with that tiny (but difficult-to-figure-out) change, we're done! 随着这一微小(但难以弄清楚)的变化,我们已经完成了!

The answer given by Mark Amery is almost complete, however it is missing the sweet spot and does not address the 'no extension is given in the request thus negotiation fails with alternatives. 马克·阿梅里给出的答案几乎已经完成,但它没有找到最佳位置,并没有解决“请求中没有给出扩展,因此协商失败的替代方案。

You can resolve this error by adding the follwing config-snippets: 您可以通过添加以下config-snippets来解决此错误:

Your PHP config should be something like this: 你的PHP配置应该是这样的:

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

Do NOT use AddType application/x-httpd-php .php or any other AddType 不要使用AddType application/x-httpd-php .php或任何其他AddType

And your additional config should be like this: 你的附加配置应该是这样的:

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

If you do use AddType you will get errors like this: 如果您使用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

As you can see, it does find index.php, however it does not use this alternative as it cannot match the Accept: image/* to application/x-httpd-php . 正如您所看到的,它确实找到了index.php,但它没有使用此替代方法,因为它无法匹配Accept: image/*application/x-httpd-php If you request /index.php/1/2/3/4 it works fine. 如果你请求/index.php/1/2/3/4它工作正常。

The reason for this I found in the source code of the mod_negotiation module. 我在mod_negotiation模块的源代码中找到了这个的原因。 I was trying to find out why Apache would work if the .php type was 'cgi' but not otherwise (hint: application/x-httpd-cgi is hardcoded..). 我试图找出为什么如果.php类型是'cgi'而Apache不会工作的原因(提示: application/x-httpd-cgi是硬编码的......)。 While in the source i noticed that apache would only see the file as a match if the Content-Type of that file matched the Accept header, or if the Content-Type of that file was empty. 在源代码中,我注意到如果该文件的Content-Type与Accept标头匹配,或者该文件的Content-Type为空,则apache只会看到该文件为匹配项。

If you use the SetHandler than apache won't see the .php files as application/x-httpd-php , but unfortunatly, many distro's also define this in the /etc/mime.types file. 如果使用SetHandler而不是apache,则不会将.php文件看作application/x-httpd-php ,但不幸的是,许多发行版也在/etc/mime.types文件中定义了这个。 So to be sure, just add the RemoveType .php to your config if this bug is bothering you. 所以可以肯定的是,如果这个bug困扰你,只需将RemoveType .php添加到你的配置中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM