Mandrill ingressが`raw_msg`欠落イベントを422で正しく拒否するよう修正
Mandrillインバウンドイベントのmsg.raw_msgが欠落している場合に発生していた500エラーを修正し、既存の不正ペイロード処理と一貫した422レスポンスを返すようになりました。
背景
Mandrillのingressは、受信したペイロードが「JSONのオブジェクトの配列」として解析できることを検証していましたが、各inboundイベントのmsgオブジェクト内にraw_msgが含まれているかどうかは検証していませんでした。この検証の抜けが、クラッシュの根本原因でした。
#57462が報告する通り、{ event: "inbound", msg: {} }のような正規署名済みだがraw_msgを持たないイベントを受信すると、event.dig("msg", "raw_msg")はnilを返します。このnilがそのままActionMailbox::InboundEmail.create_and_extract_message_id!に渡され、message_id.rb内でStringへの暗黙変換が試みられTypeErrorが送出されていました。結果として500レスポンスが返り、Mandrillの不正ペイロード処理が422を返す既存の動作と矛盾していました。
技術的な変更
raw_emailsメソッドにmsgオブジェクトおよびraw_msgの型チェックを追加し、不正な値を早期に検出して既存の例外に統一しました。
変更前:
def raw_emails
events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
end
変更後:
def raw_emails
events.select { |event| event["event"] == "inbound" }.collect do |event|
message = event["msg"]
raise MalformedEventsError unless message.is_a?(Hash)
message["raw_msg"].tap do |raw_email|
raise MalformedEventsError unless raw_email.is_a?(String)
end
end
end
変更のポイントは2段階のバリデーションです。まずevent["msg"]がHashであることを確認し、次にmessage["raw_msg"]がStringであることを確認します。いずれかの条件を満たさない場合は既存の MalformedEventsError を送出し、コントローラのrescue節が422レスポンスを返します。
あわせて、msgが空のオブジェクト({})を含むinboundイベントを送信するテストケースが追加されています。assert_no_differenceでActionMailbox::InboundEmailレコードが作成されないことを、assert_response :unprocessable_contentで422が返ることを検証しています。
設計判断
新たな例外クラスを導入せず、既存のMalformedEventsErrorに統一する方針が採られました。
Mandrillのingressはすでにペイロードの形式不正(非配列、配列要素が非Hashなど)に対してMalformedEventsErrorを使用しており、これを422に変換するrescue節が整備されています。raw_msgの欠落も同じ「不正なペイロード」として分類することで、エラー処理のパスを増やさずに一貫した動作が実現されています。また、event.dig("msg", "raw_msg")をnilチェックなしで通過させていた既存の設計に対し、型チェック(is_a?(Hash)、is_a?(String))を使うことで、nilだけでなく予期しない型全般に対して堅牢な実装となっています。
まとめ
本PRは、Mandrillインバウンドイベント処理における型チェックの抜けを最小限のコード変更で修正したものです。既存のMalformedEventsErrorを再利用する設計により、エラー処理の一貫性を保ちながら、raw_msg欠落によるクラッシュを422への正常な拒否応答へと変えています。