LWP::UserAgentでgzip,deflateエンコーディングを使うときに文字コード変換が原因でデコードに失敗することについて
タイトルがアレですが、LWPでContent-Encodingがgzipのコンテンツを展開するときに、コンテンツのcharsetに合わせた文字コード変換が動いてしまって、そのときに変換できない文字があるとundefを返すので困った! という話です。
LWP::UserAgentというか、HTTP::MessageはContent-Encodingがgzipなコンテンツのデコードに対応しているので、リクエスト時にHTTP::HeadersにAccept-Encoding: gzip,deflateを設定して送り、戻ってきたHTTP::Responseのdecoded_content()を呼べばOKなのですが、このときに文字コードの変換が同時に行われてます(2回目)。それで、この文字コードの変換はEncode::decodeで行われているのですが、decodeのoctets引数にEncode::FB_CROAK | Encode::LEAVE_SRC(エラー時にdieするオプション)を固定で渡していて、gzipの展開も含めたデコード処理全体でevalによるエラーハンドリングをしているので、2ちゃんねるのようなキャラクターセットにあっていない不正な文字コードが入りまくりのページでは、リクエストの取り出しが全体的に失敗してしまいます。
対策(案)
decoded_contentのオプションで、charset => 'none'を渡すと文字コードの変換が動かないので、それでまずgzipだけをデコードして、その後自分でcharsetにあった文字コードに変換しました。
一部ですが、こんな感じ。
my $decoded_content = \$response->decoded_content(charset => 'none'); my ($ct, %ct_param); if (my @ct = HTTP::Headers::Util::split_header_words($response->header("Content-Type"))) { ($ct, undef, %ct_param) = @{$ct[-1]}; $ct = lc($ct); } if ($ct && $ct =~ m{^text/}) { my $charset = $ct_param{charset} || 'sjis'; # sjis $decoded_content = \Encode::decode($charset, $$decoded_content); }
decodeにオプション指定なしだと、不正な文字は0xfffdに置き換えられるので、後は自分でどうこう処理します。