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::UrlEncoded | application/x-www-form-urlencoded(標準)| |
HTTP::Body::MultiPart | multipart/form-data |
HTTP::Body::OctetStream | 上のどちらでもなかったら |
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