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
とかってメッセージ出てるけど無視していいかな…)
やったね。