Action MailboxのMandrillインテグレーションで不正な形式のペイロードを422で返すよう修正
Mandrillインテグレーションにおいて、正当なJSONだが想定外の形状を持つmandrill_eventsペイロードが500エラーを引き起こしていた問題を修正しました。MalformedEventsErrorを導入してペイロードの形状検証を行うことで、既存の無効JSON処理と一貫した422レスポンスを返すようになります。
背景
Action MailboxのMandrillインテグレーションは、不正なJSONに対してはJSON::ParserErrorを捕捉して422を返す処理を持っていましたが、構文的に正しいJSONでも形状が誤っている場合は保護されていませんでした。
具体的には、mandrill_eventsパラメータにnull、スカラー値、オブジェクト(Hash)、あるいは非オブジェクトを含む配列などを渡した場合、eventsメソッドがそのまま値を返してしまいます。後続のraw_emailsメソッドで.selectを呼び出した際に、nilに対してはNoMethodError: private method 'select' called for nil:NilClassが、非Hashの配列要素に対してはTypeErrorが発生し、未処理の500エラーとなっていました。この挙動は、無効なJSONが422を返すという既存の設計と一貫しておらず、エラーの取り扱いに穴がある状態でした。
技術的な変更
eventsメソッドにペイロードの形状検証を追加し、不正な形状の場合にMalformedEventsErrorを送出するよう変更しました。この例外はコントローラのrescue節でJSON::ParserErrorと並んで捕捉されます。
変更前:
def events
JSON.parse params.require(:mandrill_events)
end
変更後:
class MalformedEventsError < StandardError
def initialize(message = "Malformed Mandrill events payload")
super
end
end
def events
JSON.parse(params.require(:mandrill_events)).tap do |parsed|
raise MalformedEventsError unless parsed.is_a?(Array) && parsed.all?(Hash)
end
end
さらにcreateアクションのrescue節にMalformedEventsErrorを追加し、両エラーを同一のパスで処理します。
rescue JSON::ParserError, MalformedEventsError => error
logger.error error.message
head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
parsed.is_a?(Array) && parsed.all?(Hash)という二段階の検証により、null・スカラー・Hashといったトップレベルの型ミスマッチと、配列内に文字列などの非Hash要素が混在するケースの両方を捕捉できます。
テスト面では、以下の3つのケースを網羅するテストが追加されました。
- JSONオブジェクト(Hash)を渡した場合
-
nullを渡した場合 - 非Hashを含む配列を渡した場合
いずれもassert_no_differenceとassert_response :unprocessable_contentで、InboundEmailが作成されず422が返ることを確認しています。
また、テスト環境のセットアップも改善されています。MANDRILL_INGRESS_API_KEY・MAILGUN_INGRESS_SIGNING_KEY・RAILS_INBOUND_EMAIL_PASSWORDの各環境変数が、ファイルの先頭でグローバルに設定される形式から、setup/teardownブロックで一時的に設定・復元する形式に変更されました。これにより、テスト間の環境変数の干渉が防止されます。
設計判断
コントローラのプライベートスコープ内に専用の例外クラスを定義する設計が採用されました。
MalformedEventsErrorはInboundEmailsController内のprivateセクションに定義されており、外部への公開を最小限に抑えています。StandardErrorを継承し、デフォルトメッセージを持つシンプルな構造にすることで、logger.error error.messageによる既存のログ出力フローをそのまま活用できます。検証ロジックをeventsメソッドのtapブロック内に閉じ込め、raw_emailsの処理には手を加えない点も、変更範囲を最小化する判断といえます。
まとめ
本PRは、JSON::ParserErrorの捕捉だけでは不十分だったペイロード検証の穴を、最小限の変更で塞いだ修正です。専用の例外クラスを導入して既存のrescue節に追加するパターンにより、エラーハンドリングの一貫性を保ちつつ、Mandrillのペイロード検証を「JSONとして正しいこと」から「Hashの配列であること」へと強化しました。