11月 14
Posted at 12:32 by Hiro

ちいッターがTwitterと連携する部分はZend_Service_Twitterを使って実装しているのですが、開発中にZend_Rest_Client_Resultクラスに潜んでいる悩まされたので備忘録として残しておきます。Zend_Service_TwitterはTwitter REST APIにアクセスするためのRESTクライアントを提供するモジュールで、Zend_Rest_Clientが実装に使われているのですが、Zend_Rest_Clientがレスポンス(XMLデータ)を格納するために使うZend_Rest_Client_Resultのコンストラクタにバグがあるようです。

問題が起きるのは、REST APIから返ってきたレスポンスがXMLとして不正だった場合で、Zend FrameworkのIssue Trackerにもレポートがあがっており、パッチも提示されています。
[#ZF-6265] Zend Rest Result.php does not handle invalid XML properly – error handlers are ignored

Zend_Rest_Client_Resultのコンストラクタは以下のようになっています。

42
43
44
45
46
47
48
49
50
51
public function __construct($data)
{
    set_error_handler(array($this, 'handleXmlErrors'));
    $this->_sxml = simplexml_load_string($data);
    if($this->_sxml === false) {
        $this->handleXmlErrors(0, "An error occured while parsing the REST response with simplexml.");
    } else {
        restore_error_handler();
    }
}

45行目のsimplexml_load_string()にXMLとして不正なデータが渡った場合、44行目のset_error_handler()で設定されたhandleXmlErrors()がsimplexml_load_string()関数の内部で呼び出されます。handleXmlErrors()の実装も見ておきましょう。

63
64
65
66
67
68
public function handleXmlErrors($errno, $errstr, $errfile = null, $errline = null, array $errcontext = null)
{
    restore_error_handler();
    require_once "Zend/Rest/Client/Result/Exception.php";
    throw new Zend_Rest_Client_Result_Exception("REST Response Error: ".$errstr);
}

65行目のrestore_error_handler()の呼び出しは、コンストラクタでset_error_handler()によりセットしたエラーハンドラを元に戻すことを意図しているようですが、restore_error_handler関数のマニュアルにも書かれているように、65行目のrestore_error_handler()は無視されてしまいます。結果として、Zend_Rest_Client_Result_Exceptionの例外がsimplexml_load_string()の呼び出し元であるコンストラクタの45行目に伝搬され、49行目のrestore_error_handler()は呼び出されることなく(handleXmlErrors()がエラーハンドラにセットされたままの状態で)、Zend_Rest_Client_Resultのコンストラクタの外へ例外が送出されてしまいます。

どういう時にこの問題が表面化するかというと、例えばアプリケーションでZend_Loader_Autoloaderを使っているような場合。Zend_Loader_Autoloaderには、クラスの命名規則に基づいたファイルが読み込めるかを事前にチェックするisReadable()関数があります。

163
164
165
166
167
168
169
170
public static function isReadable($filename)
{
    if (!$fh = @fopen($filename, 'r', true)) {
        return false;
    }
    @fclose($fh);
    return true;
}

isReadable()に渡されたファイル名$filenameが存在しなかった場合(Zend_Loader_Autoloaderでは頻繁に起こります)、165行目のfopen()関数内でエラーハンドラが呼び出されます。これが、Zend_Rest_Client_Resultのコンストラクタで例外が発生した後だった場合どうなるでしょう?Zend_Rest_Client_ResultのhandleXmlErrors()がエラーハンドラとして呼び出され、Zend_Rest_Client_Result_Exceptionの例外が送出されます。そして例外はZend_Loader_Autoloader::isReadable()の呼び出し元に伝搬するのです。

開発中に原因を究明すべく、アプリケーションのトップレベルで例外を捕捉して、例外送出時のスタックを表示させてみたのがこちら。

  • Zend_Rest_Client_Result::handleXmlErrors()
  • Zend_Loader::isReadable(“/…/views/helpers/HeadMeta.php”, “r”, true)
  • Zend_Loader_PluginLoader::load(“/…/views/helpers/HeadMeta.php”)
  • Zend_View_Abstract::_getPlugin(“HeadMeta”)
  • Zend_View_Abstract::getHelper(“helper”, “headMeta”)
  • Zend_View_Abstract::__call(“headMeta”)
  • Zend_View::headMeta(“headMeta”, Array)
  • ErrorController::errorAction()
  • Zend_Controller_Action::dispatch()
  • Zend_Controller_Dispatcher_Standard::dispatch(“errorAction”)
  • Zend_Controller_Front::dispatch(Zend_Controller_Request_Http, Zend_Controller_Response_Http)

例外が発生したのでエラーコントローラが起動して、エラーページのレンダリング時にHeadMetaのビューヘルパーが呼び出されているのですが、「なぜZend_Rest_Client_Result::handleXmlErrors()が?」と最初はまったくわからず悩みまくっていました。

この問題は、Zend_Service_Twitterに限った話ではなく、Zend_Rest_Clientを使っているすべてのモジュールに影響を及ぼすということです。V1.7.8で見つかっているバグなのに最新のV1.9.5でも反映されていないのは非常に残念ですが、とりあえずZend FrameworkでRESTクライアントを使う場合は、前述のIssue Trackerで紹介されているパッチを充てておくべきでしょう。


2 Responses to “Zend_Rest_Client_Resultクラスのバグに悩まされた”

  1. 1. SatoruYoshida Says:

    コードを確認したところ、確かに今の restore_error_handler の配置には問題があるので修正しておきました。ただし、各関数の互換性維持のため、 1.10.0 or later でリリースされます。

  2. 2. Hiro Says:

    SatoruYoshidaさん>
    コメントありがとうございます。IssueTrackのコメントも読みました。V1.10.0でアップデートされることを願っています。

コメントをどうぞ