#49106 [NEW]: PHP incorrectly sets no_local_copy=1 on response as Apache 2 module

2009-07-29 Thread n dot sherlock at gmail dot com
From: n dot sherlock at gmail dot com
Operating system: Vista 64 bit, Ubuntu 
PHP version:  5.3.0
PHP Bug Type: Apache2 related
Bug description:  PHP incorrectly sets no_local_copy=1 on response as Apache 2 
module

Description:

If PHP (5.3.0) is running as an (Apache 2) module, it currently sets
no_local_copy to 1 on the response it sends to Apache
(sapi/apache2handler/sapi_apache2.c:463). It looks like this flag was set
to disallow Apache from erroneously creating its own "304 Not Modified"
responses based on the ETag or Last-Modified-Date of the PHP scripts'
sourcecode itself, which would result in stale pages being served if the
scripts' output changes over time.

But there's a serious side effect of setting this flag in combination with
Apache's mod_cache. If the browser makes a conditional request for a cached
PHP document, but the document is expired in the cache, mod_cache correctly
passes on the conditional request to PHP. If the PHP script responds with a
"304 Not Modified" code, mod_cache should generate a 304 response for the
browser. But due to no_local_copy, Apache is denied from creating a 304
code in response to a request for a PHP document. This forces it to resend
the (still valid) body of the PHP document from the cache with a 200 code.

But setting "no_local_copy=1" is not needed anyway. Just below the
r->no_local_copy=1 line in sapi_apache2.c is a series of calls to
apr_table_unset which remove any headers that Apache might have generated
based on the PHP source itself and could be using to accept conditional
requests. Starting at line 468 in php_apache_request_ctor, we have:

apr_table_unset(r->headers_out, "Content-Length");
apr_table_unset(r->headers_out, "Last-Modified");
apr_table_unset(r->headers_out, "Expires");
apr_table_unset(r->headers_out, "ETag");

It seems to me that removing the r->no_local_copy=1 will therefore not
result in erroneous "304 Not Modified" responses being sent by Apache for
PHP scripts.

At the moment if you request a mod_cache'd PHP page which itself sends no
special caching directives (e.g. an empty script), the reply from the
server is (trimmed to include only cache-relevant directives):

Status=OK - 200
Date=Sun, 26 Jul 2009 10:07:58 GMT

PHP has correctly suppressed the generation of Last-Modified-Date and ETag
headers based on the source of the script itself. No conditional request is
possible and you won't get a stale page.

Now, if you request a PHP document that does set "ETag", such as the
attached code "index.php", the response from the server is:

Status=OK - 200
Date=Sun, 26 Jul 2009 10:11:02 GMT
Etag="ComputedETag"
Expires=Tue, 25 Aug 2009 10:11:02 GMT

And the error log shows that the script correctly returned a 200 response
code to Apache. Now if you press "refresh" in Firefox, the browser sends
this request:

If-None-Match="ComputedETag"
Cache-Control=max-age=0

This is a conditional get which will also result in Apache revalidating
its cache (since max-age=0). So Apache passes the conditional request onto
PHP, and PHP sends back a 304 Not Modified response. But due to
no_local_copy, mod_cache cannot send a 304, it responds to the browser
with:

Status=OK - 200
Date=Sun, 26 Jul 2009 10:11:35 GMT
Etag="ComputedETag"
Expires=Tue, 25 Aug 2009 10:11:35 GMT

So, I removed the line that sets no_local_copy in my PHP. This had no
impact on the way that the empty PHP document that sets no cache directives
was served, Apache never served erroneous 304 responses because it never
saw ETag or Last-Modified headers based on the PHP source. But the
ETag-conditional request for index.php by the browser now gives the correct
304 response code:

Status=Not Modified - 304
Date=Sun, 26 Jul 2009 10:16:23 GMT
Etag="ComputedETag1"
Expires=Tue, 25 Aug 2009 10:16:23 GMT

Reproduce code:
---



-- 
Edit bug report at http://bugs.php.net/?id=49106&edit=1
-- 
Try a snapshot (PHP 5.2):
http://bugs.php.net/fix.php?id=49106&r=trysnapshot52
Try a snapshot (PHP 5.3):
http://bugs.php.net/fix.php?id=49106&r=trysnapshot53
Try a snapshot (PHP 6.0):
http://bugs.php.net/fix.php?id=49106&r=trysnapshot60
Fixed in SVN:
http://bugs.php.net/fix.php?id=49106&r=fixed
Fixed in SVN and need be documented: 
http://bugs.php.net/fix.php?id=49106&r=needdocs
Fixed in release:
http://bugs.php.net/fix.php?id=49106&r=alreadyfixed
Need backtrace:  
http://bugs.php.net/fix.php?id=49106&r=needtrace
Need Reproduce Script:   
http://bugs.php.net/fix.php?id=49106&r=needscript
Try newer version:   
http://bugs.php.net/fix.php?id=49106&r=oldversion
Not developer issue: 
h

#49106 [Ana]: PHP incorrectly sets no_local_copy=1 on response as Apache 2 module

2010-01-26 Thread n dot sherlock at gmail dot com
 ID:   49106
 User updated by:  n dot sherlock at gmail dot com
 Reported By:  n dot sherlock at gmail dot com
 Status:   Analyzed
 Bug Type: Apache2 related
 Operating System: *
 PHP Version:  5.*, 6
 New Comment:

minfrin, the caching headers ETag and Last-Modified are added by Apache
before PHP gets to run, and whether mod_cache is turned on or not. They
aren't mod_cache specific.


Previous Comments:


[2010-01-26 16:56:49] ras...@php.net

I have been looking at this code a bit this morning.  It does indeed 
look like the no_local_copy is not needed here since both Last-Modified

and ETag that may be present prior to PHP being executed are removed.

And to minfrin, I think PHP does need to remove any ETag or Last-
Modified headers that are generated prior to PHP execution.  It simply

makes no sense for Apache to generate an ETag for a request prior to 
PHP executing on that request.  How could that possibly be valid?



[2010-01-26 15:03:28] minfrin at sharp dot fm

The httpd mod_cache is designed to work as a self contained cache that
bolts onto the front of the server (or with httpd v2.3+, can be inserted
anywhere in the httpd filter stack for more targeted caching).

In theory, php shouldn't be touching any of the cache fields in
request_rec at all, nor should php be trying to obscure any HTTP headers
if it detects caching has been enabled.

It should be possible for a php script to support conditional requests,
in other words php should be able to detect the If-None-Match header and
respond with 200 OK or 304 Not Modified as appropriate.

Does anyone know what problem was being solved that made php want to
care as to whether mod_cache was present?



[2009-07-30 02:40:08] n dot sherlock at gmail dot com

Description:

If PHP (5.3.0) is running as an (Apache 2) module, it currently sets
no_local_copy to 1 on the response it sends to Apache
(sapi/apache2handler/sapi_apache2.c:463). It looks like this flag was
set to disallow Apache from erroneously creating its own "304 Not
Modified" responses based on the ETag or Last-Modified-Date of the PHP
scripts' sourcecode itself, which would result in stale pages being
served if the scripts' output changes over time.

But there's a serious side effect of setting this flag in combination
with Apache's mod_cache. If the browser makes a conditional request for
a cached PHP document, but the document is expired in the cache,
mod_cache correctly passes on the conditional request to PHP. If the PHP
script responds with a "304 Not Modified" code, mod_cache should
generate a 304 response for the browser. But due to no_local_copy,
Apache is denied from creating a 304 code in response to a request for a
PHP document. This forces it to resend the (still valid) body of the PHP
document from the cache with a 200 code.

But setting "no_local_copy=1" is not needed anyway. Just below the
r->no_local_copy=1 line in sapi_apache2.c is a series of calls to
apr_table_unset which remove any headers that Apache might have
generated based on the PHP source itself and could be using to accept
conditional requests. Starting at line 468 in php_apache_request_ctor,
we have:

apr_table_unset(r->headers_out, "Content-Length");
apr_table_unset(r->headers_out, "Last-Modified");
apr_table_unset(r->headers_out, "Expires");
apr_table_unset(r->headers_out, "ETag");

It seems to me that removing the r->no_local_copy=1 will therefore not
result in erroneous "304 Not Modified" responses being sent by Apache
for PHP scripts.

At the moment if you request a mod_cache'd PHP page which itself sends
no special caching directives (e.g. an empty script), the reply from the
server is (trimmed to include only cache-relevant directives):

Status=OK - 200
Date=Sun, 26 Jul 2009 10:07:58 GMT

PHP has correctly suppressed the generation of Last-Modified-Date and
ETag headers based on the source of the script itself. No conditional
request is possible and you won't get a stale page.

Now, if you request a PHP document that does set "ETag", such as the
attached code "index.php", the response from the server is:

Status=OK - 200
Date=Sun, 26 Jul 2009 10:11:02 GMT
Etag="ComputedETag"
Expires=Tue, 25 Aug 2009 10:11:02 GMT

And the error log shows that the script correctly returned a 200
response code to Apache. Now if you press "refresh" in Firefox, the
browser sends this request:

If-None-Match="ComputedETag"
Cache-Control=max-age=0

This is a conditional get which will also result in Apache revalidating
its cache (since max-age=0). So Apache passes the conditional requ