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>test.html , 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 directive, 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!
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:
Your PHP config should be something like this:
<FilesMatch "\.ph(p3?|tml)$">
SetHandler application/x-httpd-php
</FilesMatch>
Do NOT use AddType application/x-httpd-php .php or any other 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:
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. If you request /index.php/1/2/3/4 it works fine.
The reason for this I found in the source code of the mod_negotiation module. 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..). 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.
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. So to be sure, just add the RemoveType .php to your config if this bug is bothering you.
Related
I have the following (simplified) folder/file structure:
/.htaccess
/test.php
/api/web/index.php
And the following directive in apache config:
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
SetInputFilter DEFLATE
</IfModule>
</IfModule>
I am sending a POST request with a gzipped body with the appropiated headers:
POST /test.php HTTP/1.1
Host: 192.168.1.248
Authorization: Bearer ed717c077e4bf81201196011adb457731b24e19d
Content-Type: application/json
Content-Encoding: gzip
And I have the following config for the .htaccess file:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^api/(.*) api/web/index.php/$1 [NC,L]
The issue is, if I post to /test.php, everything works as expected, the body is deflated and I can access decompressed contents just right.
However if I post to something that gets redirected (/api/ or /api/v1/project) the index.php script does not get the body decompressed.
I think it must be related to the RewriteRule directive ignoring the SetInputFilter directive, but, how can I avoid this situation?
I tried to add the SetInputFilter directive directly in the .htaccess without solving the issue (may be it was not in the right place?).
Do you know how can I solve this?
Indeed, there is a problem. The first thing I did to investigate deeper was to log traces of concerned modules (rewrite, filter and deflate).
mod_rewrite logs were OK, nothing suspicious there. To make sure everything was really OK, I looked at the very last version of its source code. And again, nothing suspicious regarding encoding/decoding (nor http request/response headers, more generally).
So I started thinking the problem may come from either filter or deflate module, even if it may also come from somewhere else. To confirm/infirm what I thought, I looked at those modules logs. Quickly, I was able to see a difference between the two test cases: with or without mod_rewrite involved.
mod_rewrite not involved
mod_deflate.c(1421): [client 127.0.0.1:53000] AH01393: Zlib: Inflated 35 to 41 : URL /test.php
mod_filter.c(188): [client 127.0.0.1:53000] Content-Type condition for 'deflate' matched
I took this one as a reference to compare the next case below
mod_rewrite involved
mod_filter.c(188): [client 127.0.0.1:53002] Content-Type condition for 'deflate' matched
Interesting. Actually, it looked like mod_deflate was the problem. I suspected its action was after the right moment. That's the reason why you don't see it in action, here, in this case.
Solution
So far, so good. So... what ? Well, a quick search on the known bugs list of Apache, with keywords mod_deflate too late, gave me by chance what I was searching for. This ticket called mod_deflate adjusts the headers "too late", states the following:
When mod_deflate is used to inflate, it must adjust the request
headers (e.g. it needs to remove the "Content-Length" header and
adjust the "Content-Encoding" header).
Currently mod_deflate adjusts the headers when the request body is
read. But this is too late. For example, if a content generator module
needs to look at the request headers before reading the request body,
the content generator module "sees" the old (unmodified) headers.
mod_deflate should adjust the headers in an early stage, for example
in a fixup hook (ap_hook_fixups).
Eureka ! This is exactly the problem we're facing. Now, good news is there is a patch to overcome this issue. Bad news: it is not yet reviewed/accepted/merged in available versions.
You have the choice:
Apply this patch and recompile your server. It should work because the all thing makes sense. But, be careful... this may introduce other bugs/holes (that's sometimes the case, even when reviewed/accepted)
Wait for it to be included in available versions (maybe a long time, considering the ticket date). By then, use your custom deflate with php.
Update
Just tried to apply the patch and recompile mod_deflate. Looks like it's on the right track: it eats Content-Encoding header. Anyway, Content-Length is still there. Result: no yet decompressed. So, there is still something to do and adapt, but the problem is definitely in that area.
Update 2 (working)
I managed to make it work, finally. Here is the patch I applied to Apache (httpd version 2.4.34):
diff --git a/modules/filters/mod_deflate.c b/modules/filters/mod_deflate.c
index 1428460..cc8c0cb 100644
--- a/modules/filters/mod_deflate.c
+++ b/modules/filters/mod_deflate.c
## -1099,10 +1099,10 ## static apr_status_t deflate_in_filter(ap_filter_t *f,
if (!ctx) {
/* only work on main request/no subrequests */
- if (!ap_is_initial_req(r)) {
+ /*if (!ap_is_initial_req(r)) {
ap_remove_input_filter(f);
return ap_get_brigade(f->next, bb, mode, block, readbytes);
- }
+ }*/
/* We can't operate on Content-Ranges */
if (apr_table_get(r->headers_in, "Content-Range") != NULL) {
Actually, I made mod_deflate handle sub-requests too. I'm not sure it's not gonna break some other modules, but it works for your use case (it's more a proof of concept). Anyway, I proposed my patch on the ticket mentioned above. Here is a screenshot of the result:
The client is requesting an image:
GET /api/2.0/users/80.png HTTP/1.1
Host: learnwithecho.com
Proxy-Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: image/* <------------------------------ HERE'S THE IMPORTANT PART
Accept-Language: en-us
Connection: keep-alive
User-Agent: Echo/1.0.16.1 CFNetwork/672.0.2 Darwin/12.5.0
And I have a script at api/2.0/users.php (yes, PATH_INFO is on)
...
header('Content-Type: image/png');
$user = User::getUserWithID($filename);
header("Location: ".$user->getImageURL());
exit(0);
But Apache or PHP is trying to act like it knows me... and it don't. It assumes a PHP script couldn't possibly want to respond with a image/png and it throws a 406 Not Acceptable error.
Can I successfully configure Apache/PHP to respond to this request?
Can I successfully configure Apache/PHP to respond to this request?
Yes. Just use the MultiviewsMatch directive to tell Apache that it can serve .php files regardless of whether their MIME type is compatible with the Accept header:
<Files "*.php">
MultiviewsMatch Any
</Files>
From the docs, the effect is as follows:
You may finally allow Any extensions to match, even if mod_mime doesn't recognize the extension.
You need to either disable MultiViews in this context or create dummy copies of your script with extensions that tell mod_negotiation what kinds of mimetypes it can generate (not really recommended)
as-is, mod_negotiation has no way to probe for what types can be generated by users.php.
I've googled the crap out of this problem and came up with nothing, so hopefully you guys can help.
Goal
Configure a reverse proxy (using Apache's mod_proxy) to enable access to an internal PHP application over the internet using mod_proxy_html to rewrite URLs in my application.
Problem Description
Consider landing.php with only this code:
redirect.php
and redirect.php with only:
<?php
header("Location:http://internal.example.com/landing.php");
?>
with this snippet from my httpd.conf
UseCanonicalName On
LoadFile /usr/lib64/libxml2.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule xml2enc_module modules/mod_xml2enc.so
<VirtualHost *:80>
ServerName example.com
<Proxy *>
Order deny,allow
Allow from all
AllowOverride None
</Proxy>
ProxyPass / http://internal.example.com/
ProxyPassReverse / http://internal.example.com/
ProxyHTMLLinks a href #Commenting out this line fixes the problem, but I need it for rewriting
ProxyHTMLEnable On
RequestHeader unset Accept-Encoding
</VirtualHost>
When I go to http://example.com/landing.php and click "redirect.php" it should take me back to the landing.php page. Instead, I get a "This connection was reset" in Firefox, or "No data received" in Chrome. (FYI, going to http://internal.example.com/redirect.php redirects correctly.)
The Question:
Why the redirect would fail going through the reverse proxy, and how can I fix this?
Hints
I've discovered a few things that could be helpful...
I know that if I comment out "ProxyHTMLLinks a href", this will work correctly. But obviously, this is the rewrite functionality I need.
I can also change the redirect.php page to the following, this works correctly:
<?php
header("Location:http://internal.example.com/landing.php");
?>
random text
I guess this text somehow does something to the page or HTTP headers that make mod_proxy_html (or more specifically ProxyHTMLLinks) operate differently than without it.
I can also change the redirect.php page to the following and have it work:
<?php
header("Location:http://internal.example.com/landing.php");
header("Content-Type:");
?>
This works because ProxyHTMLLinks, by default, is only applied to Content-Type text/html files. However, I don't want to have to hack all calls to header("Location:...") to make this work. I don't mind changing all the calls to header("Location:..."), assuming what I'm changing is correcting a problem, not creating a hack.
Lastly, I've done some packet sniffing on the reverse proxy server and discovered that the header("Location:...") sends a HTTP/1.1 302 Not Found to the reverse proxy server, but it doesn't pass this through to the browser requesting redirect.php. When I try one of the "solutions" above, the 302 is then passed from the reverse proxy server to the computer requesting redirect.php.
My understanding is that the Location header should go to the browser, and then the browser should request the new location passed back. So it is failing because the 302 doesn't make it to the browser...
FYI, I've tried looking at the error logs to see if mod_proxy_html is failing somewhere, but I don't see anything, though I'm open to specific suggestions with regards to logging, since I'm not 100% sure if I'm setting the logging up correctly.
Sorry this is so long, just trying to be as specific as possible.
Thanks in advance!
I figured out the problem. I needed to explicitly pass the charset in the header Content-Type for this to work.
This was accomplished by adding:
AddDefaultCharset utf-8
to my Apache config file. This globally fixed all calls to header("Location:...") without having to add header("Content-Type:") or header("Content-Type:text/html;charset=utf-8") to each one of them.
In short, what I'm saying that the mod_proxy_html's ProxyHTMLLinks causes a 302 Found to not be forwarded from the reverse proxy server to the client if a) the content-type is text/html (and thus ProxyHTMLLinks) applies, b) the charset is not set and c) your page has no content passed back.
In my opinion, this is a normal scenario. Pages which process form inputs often meet all three criteria.
It's possible that for some reason this is the intended functionality, and that I'm doing something else wrong, but I can't see what that would be. At least there is an elegant workout here in case anyone finds it useful.
Okay, so I have a weird one for you all today. I'm looking into creating a custom MIME type for a .php file. I've read some pro's/con's on this and it won't be for much other than some experimentation to see what can really be done. My company's initials are TTP and so we decided it'd be kinda fun to have all of our .php pages re-written to a custom .ttp extension. I've attempted my normal cPanel X route with adding it in, and I've also tried adding the change into the .htaccess file. It work's perfectly fine until I change the application type to anything php.
AddType text/html ttp // works
AddType text/x-php ttp // doesn't work
AddType application/x-php ttp // doesn't work
AddType application/x-http-php ttp // doesn't work
Some things that have come up was an issue that doing this renders the .php file and therefore makes it difficult for the browser to decide how to handle it. Any other ideas? I'm pretty sure that at the end of the day this won't be something the company will do, but they wanted to see if any experiment I could run will work.
The browser doesn't handle PHP. Content-Type doesn't matter here.
Look at your CGI or module configuration, to configure PHP to handle more than .php. For PHP as a module:
<FilesMatch \.ttp$>
SetHandler application/x-httpd-php
</FilesMatch>
Handlers are specified with AddHandler. The mod_php handler is php5-script.
And the browser never handles PHP.
It should be so simple. I've followed every tutorial and forum I could find, yet I can't get it to work. I simply want to build a RESTful API in PHP on Apache2.
In my VirtualHost directive I say:
<Directory />
AllowOverride All
<Limit GET HEAD POST PUT DELETE OPTIONS>
Order Allow,Deny
Allow from all
</Limit>
</Directory>
Yet every PUT request I make to the server, I get 405 method not supported.
Someone advocated using the Script directive, but since I use mod_php, as opposed to CGI, I don't see why that would work.
People mention using WebDAV, but to me that seems like overkill. After all, I don't need DAV locking, a DAV filesystem, etc. All I want to do is pass the request on to a PHP script and handle everything myself. I only want to enable PUT and DELETE for the clean semantics.
You don't need to configure anything. Just make sure that the requests map to your PHP file and use requests with path info. For example, if you have in the root a file named handler.php with this content:
<?php
var_dump($_SERVER['REQUEST_METHOD']);
var_dump($_SERVER['REQUEST_URI']);
var_dump($_SERVER['PATH_INFO']);
if (($stream = fopen('php://input', "r")) !== FALSE)
var_dump(stream_get_contents($stream));
The following HTTP request would work:
Established connection with 127.0.0.1 on port 81
PUT /handler.php/bla/foo HTTP/1.1
Host: localhost:81
Content-length: 5
boo
HTTP/1.1 200 OK
Date: Sat, 29 May 2010 16:00:20 GMT
Server: Apache/2.2.13 (Win32) PHP/5.3.0
X-Powered-By: PHP/5.3.0
Content-Length: 89
Content-Type: text/html
string(3) "PUT"
string(20) "/handler.php/bla/foo"
string(8) "/bla/foo"
string(5) "boo
"
Connection closed remotely.
You can hide the "php" extension with MultiViews or you can make URLs completely logical with mod_rewrite.
See also the documentation for the AcceptPathInfo directive and this question on how to make PHP not parse POST data when enctype is multipart/form-data.
AllowOverride AuthConfig
try this. Authentication may be the problem. I was working with a CGI script written in C++, and faced some authentication issues when passed DELETE. The above solution helped me. It may help in your case too.
Also even if you don't get the solution for your problem of PUT and DELETE, do not stop working rather use "CORS". It is a google chrome app, which will help you bypass the problem, but remember it is a temporary solution, so that your work or experiments doesn't remain freeze for long. Obviously, you cannot ask your client to have "CORS" enabled to run your solution, as it may compromise systems security.
On linux, /etc/apache2/mods-enabled/php5.conf dans php5.load exists. If not, enables this modules (may require to sudo apt-get install libapache2-mod-php5).
IIRC the purpose of the form method attribute was to define different transport methods. Consequently, HTML 5.2 only defines GET, POST, and DIALOG methods for transport and dialog action, not how the server should process the data.
Ruby-on-rails solves this problem by using POST/GET for everything and adding a hidden form variable that defines the actual ReST method. This approach is more clumsy and error-prone, but does remove the burden from both the HTML standard and browser developers.
The form method was defined before ReST, so you cannot define ReST in HTML, even after enabling Apache and PHP because the browsers conform to HTML and therefore default to GET/POST for all non-HTML defined values. That means, when you send a form to the browser with a PUT method, the browser changes that to GET and uses that instead. The hidden variable, however, passes through everything unchanged, so you can use that to customise your form handling process.
Hope that helps
The technical limitations with using PUT and DELETE requests does not lie with PHP or Apache2; it is instead on the burden of the browser to sent those types of requests.
Simply putting <form action="" method="PUT"> will not work because there are no browsers that support that method (and they would simply default to GET, treating PUT the same as it would treat gibberish like FDSFGS). Sadly those HTTP verbs are limited to the realm of non-desktop application browsers (ie: web service consumers).