# Why does PHP set no_local_copy?

- Thread: https://externals.io/message/45097
- Original archive: https://news-web.php.net/php.internals/45097
- Started by Nicholas Sherlock on 2009-07-26T10:22:29+00:00
- Messages: 2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #45097 — Nicholas Sherlock — 2009-07-26T10:22:29+00:00

URL: https://externals.io/message/45097#45097

Hi everyone,

I'm a newbie to the codebase of both Apache and PHP, but I hope my 
message makes some sense.. :).

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). From reading some history in 
the bug tracker it seems that this change was made to stop Apache from 
sending "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 be 
passing that 304 code onto the client. But due to no_local_copy, Apache 
is denied from sending 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.

What I don't quite understand is why "no_local_copy=1" is needed. Just 
below the r->no_local_copy=1 line in sapi_apache2.c is a series of calls 
to apr_table_unset which look like they 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.

Now, if you request a PHP that does set "ETag", such as this index.php:

<?php

/* Generate our ETag. Assume that generating the ETag is
  * a whole lot less expensive than generating the content
  * (e.g. it could be based on revision counts for documents
  * from a database).
  */
$etag="\"ComputedETag\"";

header("Etag: $etag");
//Expires ages away
header("Expires: " . gmdate("D, d M Y H:i:s", `time()`
	+ 60 * 60 * 24 * 30) . " GMT");

if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
	$_SERVER['HTTP_IF_NONE_MATCH'] == $etag) {

	/* At a users' request, the cache has been bypassed, but the
	 * document is still the same. Avoid costly response generation
	 * and waste of bandwidth by just sending not-modified.
	 * (it is illegal to send a response-body by the HTTP spec).
	 */
	header('HTTP/1.0 304 Not Modified');
	
	error_log(date('r')." - Response: 304 Not Modified");
 `exit()`; //Don't generate or send the body
}	

error_log(date('r')." - Response: 200. Generated document.");

echo "Document body goes here";

?>

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. And 
the response for the first request of my "index.php" test document was 
the same. 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

Is there some other reason or situation where setting r->no_local_copy=1 
is important?

Cheers,
Nicholas Sherlock

---

## #45133 — Nicholas Sherlock — 2009-07-30T02:47:06+00:00

URL: https://externals.io/message/45097#45133 · In reply to: #45097

Nicholas Sherlock wrote:
> 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 

This is now bug #49106:

http://bugs.php.net/bug.php?id=49106

Cheers,
Nicholas Sherlock
