Catalystでフォームの値が、ある条件化において正確に取れない件

問題

ご存じのように、
$c->req->param('fieldname')としてフォームの値を参照できますが、
今構築中のアプリで、BodyParameterログをみてみると、
(※リクエスト値があるときにデバッグログに表示されるやつ)

.... 入力されていないフィールドの名前と値('')が取れていない ....

つまり、

$c->req->param('empty_field_name') => undef,
exists $c->req->params->{'empty_field_name'} => False

っておおっ??


普通は困らないのかもですが、
今はHTML::FillInFormを使う事前提の組み方なので、
空でもなんでも、値(というか、そのフィールド名)が検知できないと
色々××なことになるのです。

これはアカンということで調査。

結果

Catalyst自身の問題かなっと思ったけど.....
問題の箇所は別のところでした。

HTTPリクエスト BodyをパースするHTTP::Body::MultiPartモジュールが、
空の値をもったフィールドが取り除いてしまっていた、

というのが原因でした。。

詳細は以下に。お時間あれば一読していただければと。

Catalystのリクエスト処理を追う

CatalystはHTTPリクエストがくるとまずはprepareフェーズに入ります。
最初にcontext( あの$c )を準備して、次に、リクエストを解析するprepare_*メソッドを順次実行します。

$c->prepare_request(@argument);   # $ENVを受け取る
$c->prepare_connection;            # HTTPかHTTPSか判定してごにょ
$c->prepare_query_parameters;     # $ENV{QUERY_STRING}をパースしてparamsにセット
$c->prepare_headers;               # HTTPリクエストヘッダを解析してセット
$c->prepare_cookies;               # 受け取ったヘッダからCookieを取り出してセット
$c->prepare_path;                   # BaseURIなどをセット(PROXYで動かしても動くような処理も)
                                      #基本的には「GET」ではここまで
# On-demand parsing
$c->prepare_body unless $c->config->{parse_on_demand}; #HTTPリクエストBODYを解析してparamsにセット


Catalyst::Engine(::*)系で上のような処理を行います。
上記の問題は、POSTでしか(しかも特定のフォーム)起きていないこともあり、prepare_bodyメソッドに注目。


Catalyst::Engine

sub prepare_body {
    my ( $self, $c ) = @_;
    my $type = $c->request->header('Content-Type');

    $c->request->{_body} = HTTP::Body->new( $type, $self->read_length );

    while ( my $buffer = $self->read($c) ) {
        $c->prepare_body_chunk($buffer);
    }
}

prepare_body_chunk($buffer)

sub prepare_body_chunk {
    my ( $self, $c, $chunk ) = @_;
    $c->request->{_body}->add($chunk);
}

端折ってるけど、重要な部分を取り出すとこんな感じ。


HTTP::BODYモジュールを使用して、リクエストBodyのパースをし、
読み込んだHTTPリクエストBodyを
そのモジュールのaddメソッドに渡してパラメータを取得しています。


HTTP::Bodyをnewして、addする。ここに問題が潜んでいました。

HTTP::Bodyに潜む問題

HTTP::Bodyでは、newメソッド内で、エンコードタイプ($type)に合わせて読み込むモジュールを変えています。

読み込むモジュールエンコードタイプ
HTTP::Body::UrlEncodedapplication/x-www-form-urlencoded(標準)|
HTTP::Body::MultiPart multipart/form-data
HTTP::Body::OctetStream上のどちらでもなかったら
基本的なPOSTでは、エンコードタイプは標準と記されたものになるため、 $c->body->{_body}には、HTTP::Body::UrlEncodedが、newされます。 また、ファイルをアップロードするときなど、 form にenctype="multipart/form-data"を指定しているときには、 HTTP::Body::MultiPartがnewされることになります。 そして、addメソッドでは、それぞれのモジュールのspinメソッドが呼ばれます。 HTTP::Body::UrlEncodedは特に問題ないのですが、 (つまり、普通のPOSTでは$c->req->paramsは正常) 一方、HTTP::Body::MultiPartにありました、問題。

sub spin {
    my $self = shift;
    while (1) {
        if ( $self->{state} =~ /^(preamble|boundary|header|body)$/ ) {
            my $method = "parse_$1";
            return unless $self->$method;
        }
    }
}


MultiPartでのspinは、MultiPartとして渡ってくるデータ(*1)をパースし、
その中の実データ部分(body)を取得しています。

取得したものを $self->parse_bodyすることによって、
次のデータをセットします。


$self->{part}{done}: 読み込み完了フラグ
$self->{part}{size}: bodyのデータサイズ
$self->{part}{data}: 読み込んだデータ部分

そして、最後に handlerを呼ぶのですが、ここ注目!

sub handler {
    my ( $self, $part ) = @_;

    # skip parts without content
    if ( $part->{done} && $part->{size} == 0 ) {
        return 0;
    }else{
         $self->param( $part->{name}, $part->{data} );
    }
}

(かなり省略)


おっと、、、
$part->{size} == 0のときには、
paramにセットしないで、return 0しちゃってます。

ここだ。

結論

というわけで、

formで method="POST" enctype="multipart/form-data"
を指定してるときには、
HTTPリクエストのパーサー HTTP::Body::MultiPartの中で
sizeが0のパラメータは除去されている


というオチでした。
実際に、このreturn 0を除去したらきちっと取得できるようになりました。
うーん、、、なんでここでreturn 0することにしたのか...



(*1)備考 [POST時のHTTPリクエストのBodyの様相]

(i) application/x-www-form-urlencoded(通常)の場合

Content-Type: application/x-www-form-urlencoded

Content-Length: 58

user_name=yupug&user_sex=man

(ii) multipart/form-dataを指定したときのリクエスト(Content部分)

Content-Type: multipart/form-data; boundary=---------------------------132067928117778102532114132971
Content-Length: 1421
-----------------------------132067928117778102532114132971
Content-Disposition: form-data; name="user_name"

yupug
-----------------------------132067928117778102532114132971
Content-Disposition: form-data; name="user_sex"

man
-----------------------------132067928117778102532114132971
Content-Disposition: form-data; name="user_image"; filename="user.jpg"

Content-Type: image/jpeg

(binary)


-----------------------------132067928117778102532114132971