読者です 読者をやめる 読者になる 読者になる

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に置き換えられるので、後は自分でどうこう処理します。