ページキャッシュの不正によるページが真っ白問題ほぼ完結

何度が格闘してあきらめかけていたsymfonyのキャッシュの「ページが真っ白問題」ですが、昨日友人のFakeさんから「HEADリクエストがきたときのキャッシュなんじゃないか?」と指摘され、その方面で再調査したところビンゴでした。

つまり、ブラウザやその他のシステムから「HEAD」リクエストで特定のURLが呼び出された場合、symfonyでは標準で「ヘッダのみ」のレスポンスを応答するようになっていますが、この時に指定したURLに対するページキャッシュが存在しないorキャッシュの有効期限が切れている場合、このリクエストのレスポンスとして、つまり「ヘッダのみ=true」の状態でキャッシュが作成されてしまうのです。

ファクトリからsfWebResponseオブジェクトを生成した直後、HEADリクエストの場合はsetHeaderOnly(true)が呼び出されるようになっていました。

<?php
//
//  cache/frontend/prod/config/config_factories.yml.php内
//
  if ($this->factories['request'] instanceof sfWebRequest 
      && $this->factories['response'] instanceof sfWebResponse 
      && 'HEAD' == $this->factories['request']->getMethod())
  {  
    $this->factories['response']->setHeaderOnly(true);
  }


このリクエストを受け取った時でもキャッシュが再作成されること自体は問題ではなく、「ヘッダのみフラグがtrueになってキャッシュに保存される」ことが問題ですので、その部分のみ、自前の派生クラスで処理をオーバーライドして対応することにしました。

# キャッシュされるのがページのコンテンツ本体ではなくて、sfWebResponseオブジェクト丸ごと、という
# symfonyの仕様が問題なのかもしれません・・・。
# last-modifiedなども合わせて保存されているので便利といえば便利なのかもしれませんが・・・。

sfViewCacheManagerのsetPageCacheをオーバーライド

問題の箇所は、sfViewCacheManagerのsetPageCacheメソッドです。
自前の派生クラスを用意して、以下のように定義します。

<?php
//
// lib/xnniViewCacheManager.class.php
//
class xnniViewCacheManager extends sfViewCacheManager {
    //
    // setPageCacheをオーバーライド
    //
    public function setPageCache($uri) {
        if (sfView :: RENDER_CLIENT != $this->controller->getRenderMode()) {
            return;
        }
        // save content in cache
        $web_response_data = $this->context->getResponse();
        $headeronly = $web_response_data->isHeaderOnly();
        $web_response_data->setHeaderOnly(false);
        $saved = $this->set(serialize($web_response_data), $uri);
        $web_response_data->setHeaderOnly($headeronly);

        if ($saved && sfConfig :: get('sf_web_debug')) {
            $content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array (
                'response' => $this->context->getResponse(),
                'uri' => $uri,
                'new' => true
            )), $this->context->getResponse()->getContent())->getReturnValue();

            $this->context->getResponse()->setContent($content);
        }
    }
}

変更しているのは、「// save content in cache」コメント以下の数行です。responseオブジェクトに現在設定されているHeaderOnlyフラグを一時待避して、キャッシュにはHeaderOnly=falseの状態で保存されるようにしています。

自前の派生クラスをファクトリに登録

この派生クラスをプロジェクトでViewCacheManagerとして使用するためには、factories.ymlに設定を追加する必要があります。

#
# app/frontend/config/factories.yml
#
all:
  view_cache_manager:
    class: xnniViewCacheManager

allの設定などで、view_cache_managerというエントリを追加します。

symfony cc でキャッシュをクリアすると、次からこのファクトリの設定が有効になります。



# 何か理由があってこのようなキャッシュシステムになっているのかもしれません。
# なので、symfonyの不具合といえるのかどうか、判断できないです。
# 同じように困っている方が多ければ「不具合」と言えるのかもしれませんね。


本家にticketをポスト

この件、wassrにてmasakiさん、あとはてブにてid:tsukimiyaさんからご指摘いただいたので、本家にticketとしてポストしました。

http://trac.symfony-project.org/ticket/6715


【2009/08/24追記】
milestoneが1.2.9にセットされていたので、このバージョンで修正されたかどうか改めて確認してみることにします。

【2009/10/20追記】
milestoneが一旦クリアされていたのですが、1.3.0 alpha2でfixedになったようなので、改めて確認してみます。