Setting the Expires header to any value using drupal_add_http_header() causes the page caching subsystem to fail, always causing misses and sending X-Drupal-Cache: MISS headers.

This happens regardless of whether you set it to a value or unset it using FALSE.

This happens regardless of whether it is called in hook_boot, hook_init, or hook_page_alter.

Comments

Steven Merrill’s picture

This occurs on Drupal 7.22 and 7.26.

bmcmurray’s picture

I've done some sleuthing and here is what I think is happening:

The function drupal_page_set_cache() (includes/common.inc), specifically lines 5187:5190, checks for the ‘Expires’ header and then calls strtotime() on the value of that, expecting the value to be a string that can be converted to a time. Even though the documentation for drupal_get_http_header() (https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/drup...) states that it’s valid to set a header value to FALSE to unset it, the Expires header appears to be an undocumented special case. Calling strtotime(FALSE) returns a non-numeric (non-unix timestamp) result, causing the cache set to have no lifetime, essentially allowing it to immediately expire. Memcache allows an expire time of 0 to be set to store an item in the cache “permanently” (as long as possible until pushed out of memory or force flushed) -- http://www.php.net/manual/en/memcache.set.php. Attempting to set the Expires header to 0 does not resolve this, but instead causes the Expires header to appear in returns as: Expires: 0 and strtotime(0) returns a non-numeric result.

Steven Merrill’s picture

Title: Setting the Expires header to any value disables page caching » Unsetting the Expires header or a non-strtotime()-parsable value disables page caching
Alexander Allen’s picture

My observations are exactly the same as @bmcmurray. While using the FALSE as a second parameter does work as advertised by drupal_add_http_header() for most headers, there is that particular use case inside drupal_page_set_cache() where if one particular header is named expires, the value of $cache->expire = strtotime($value); becomes strtotime(FALSE), which in turns ends up setting $cache->expire as FALSE. So the header is removed, but the page cache is defeated and a full drupal bootstrap / render done for the request. Here are the relevant drupal_page_set_cache() lines for reference:


    // Restore preferred header names based on the lower-case names returned
    // by drupal_get_http_header().
    $header_names = _drupal_set_preferred_header_name();
    foreach (drupal_get_http_header() as $name_lower => $value) {
      $cache->data['headers'][$header_names[$name_lower]] = $value;
      if ($name_lower == 'expires') {
        // Use the actual timestamp from an Expires header if available.
        $cache->expire = strtotime($value);
      }
    }

Also confirming smerrill's observation that setting the value to an integer on drupal_add_http_header() doesn't solve the issue (something I was hoping for).

Version: 7.26 » 7.x-dev

Core issues are now filed against the dev versions where changes will be made. Document the specific release you are using in your issue comment. More information about choosing a version.