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