モンモンブログ

技術的な話など

Rails でリクエストパラメータに Shift_JIS 文字列が渡されるとエラるので nkf で強制変換するモンキーパッチあてて回避

SendGrid 宛に届いたメールを Rails の Mailbox で受け取って、開発チームの Slack へ通知するようにしてるのですが、

メールの文字コードが Shift_JIS だった場合に ActionController::BadRequest エラーが発生してしまう問題に悩まされていました。エラーメッセージの冒頭はこんな感じ。

An ActionController::BadRequest occurred in inbound_emails#create:

  Invalid request parameters: Invalid encoding for parameter:
…

エラーは ApplicationMailbox に到達する以前、Rack Middleware のどこかで発生しているようで捕捉も難しかったのですが、exception_notification のエラーメールのバックトレースを頼りに解決しました。

環境

% bundle exec rails --version
Rails 6.1.4.1
% bundle info exception_notification
  * exception_notification (4.4.3)
        Summary: Exception notification for Rails apps
        Homepage: https://smartinez87.github.io/exception_notification/
        Path: /Users/monmon/ghq/github.com/mig-hld/shokutsu-api/vendor/bundle/ruby/2.7.0/gems/exception_notification-4.4.3

原因

exception_notification の Backtrace の項を見るとこんな感じ。

-------------------------------
Backtrace:
-------------------------------

  actionpack (6.1.4.1) lib/action_dispatch/request/utils.rb:39:in `check_param_encoding'
  actionpack (6.1.4.1) lib/action_dispatch/request/utils.rb:34:in `block in check_param_encoding'
  actionpack (6.1.4.1) lib/action_dispatch/request/utils.rb:34:in `each_value'
  actionpack (6.1.4.1) lib/action_dispatch/request/utils.rb:34:in `check_param_encoding'
  actionpack (6.1.4.1) lib/action_dispatch/http/request.rb:403:in `block in POST'
  rack (2.2.3) lib/rack/request.rb:69:in `fetch'
  rack (2.2.3) lib/rack/request.rb:69:in `fetch_header'
  actionpack (6.1.4.1) lib/action_dispatch/http/request.rb:398:in `POST'
  actionpack (6.1.4.1) lib/action_dispatch/http/parameters.rb:55:in `parameters'
  config/initializers/wrap_parameters.rb:11:in `process_action'
  actionpack (6.1.4.1) lib/abstract_controller/base.rb:165:in `process'
  …

check_param_encoding ってメソッドでエラー発生してるらしい。

該当箇所を見てみます。

 29       def self.check_param_encoding(params)
 30         case params
 31         when Array
 32           params.each { |element| check_param_encoding(element) }
 33         when Hash
 34           params.each_value { |value| check_param_encoding(value) }
 35         when String
                     # ↓これが false を返すために、
 36           unless params.valid_encoding?
 37             # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
 38             # ActionDispatch::Request#GET will re-raise as a BadRequest error.
                # ↓ここで例外発生
 39             raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
 40           end
 41         end
 42       end

リクエストパラメータ(foo=bar&baz=qux みたいな)の文字列ごとに String#valid_encoding? で文字コードをチェックするメソッドのようです。

ここに print デバッグや binding.pry を仕込んで調査した感じ、パラメータとして Shift_JIS 文字列が渡されてるのにも関わらずその String#encoding がなぜか #<Encoding:UTF-8> を返すために、上記コード中で String#valid_encoding? が文字コードの不一致とみなして false を返すようです。

なんでこうなのかはよく分からんけど、ええいままよ!(パパよ!)モンキーパッチを宛てて回避しちゃいました。あまりお行儀よくないかもしれないけど…

解決

先ほどの check_param_encoding をモンキーパッチで再定義します。

$ vi config/initializers/monkey_patch_action_dispatch_request_utils.rb
module ActionDispatch
  class Request
    class Utils
      def self.check_param_encoding(params)
        case params
        when Array
          params.each { |element| check_param_encoding(element) }
        when Hash
          params.each_value { |value| check_param_encoding(value) }
        when String
          # ↓↓↓追加↓↓↓
          params = NKF.nkf("--oc=UTF-8", params)
          # ↑↑↑追加↑↑↑
          unless params.valid_encoding?
            # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
            # ActionDispatch::Request#GET will re-raise as a BadRequest error.
            raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
          end
        end
      end
    end
  end
end

String#encode メソッドではうまく UTF-8 に変換できなかったりするらしいので、 NKF でパラメータを強制的に UTF-8 に変換してしまってます。(参考: RubyでUTF-8をShiftJISに変換するならnkfを使うべき - 動かざることバグの如し

テスト

どこか適当なパス (ここではテスト用に用意した /test) に、適当なパラメータ名(ここでは "hoe")で、Shift_JIS な文字列を送信してみます。

$ curl "http://localhost:3000/test?hoe=$(echo テスト | nkf --sjis)"

ログを確認

INFO -- : Started GET "/test?hoge=eXg" for 127.0.0.1 at 2021-12-11 16:18:32 +0900
log writing failed. "\x83" from ASCII-8BIT to UTF-8
INFO -- : Processing by TestController#index as */*
INFO -- :   Parameters: {"hoge"=>"\x83e\x83X\x83g"}
INFO -- : Completed 200 OK in 1ms (Allocations: 78)

確かにパラメータに Shift_JIS 文字列が渡されてるけど、例外は発生しませんでした。(なんか log writing failed とかってメッセージ出てるけど無視していいかな…)

やったね。