Action MailboxのMandrillインテグレーションで不正な形式のペイロードを422で返すよう修正

rails/rails

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_differenceassert_response :unprocessable_contentで、InboundEmailが作成されず422が返ることを確認しています。

また、テスト環境のセットアップも改善されています。MANDRILL_INGRESS_API_KEYMAILGUN_INGRESS_SIGNING_KEYRAILS_INBOUND_EMAIL_PASSWORDの各環境変数が、ファイルの先頭でグローバルに設定される形式から、setup/teardownブロックで一時的に設定・復元する形式に変更されました。これにより、テスト間の環境変数の干渉が防止されます。

設計判断

コントローラのプライベートスコープ内に専用の例外クラスを定義する設計が採用されました。

MalformedEventsErrorInboundEmailsController内のprivateセクションに定義されており、外部への公開を最小限に抑えています。StandardErrorを継承し、デフォルトメッセージを持つシンプルな構造にすることで、logger.error error.messageによる既存のログ出力フローをそのまま活用できます。検証ロジックをeventsメソッドのtapブロック内に閉じ込め、raw_emailsの処理には手を加えない点も、変更範囲を最小化する判断といえます。

まとめ

本PRは、JSON::ParserErrorの捕捉だけでは不十分だったペイロード検証の穴を、最小限の変更で塞いだ修正です。専用の例外クラスを導入して既存のrescue節に追加するパターンにより、エラーハンドリングの一貫性を保ちつつ、Mandrillのペイロード検証を「JSONとして正しいこと」から「Hashの配列であること」へと強化しました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
430d9dca

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的な変更、設計判断、まとめの「総論→各論→結論」構成が明確で、非常に理解しやすいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Action Mailboxの内部実装に関する内容で、専門知識を持つエンジニアという対象読者に完全に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが論理的に構成され、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードは、提供されたDiff情報と正確に一致しています。テストコードの改善という周辺の変更点にも触れており、PRの全体像を的確に伝えています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

JSON::ParserError, NoMethodError, rescue節など、Rails開発における技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

問題の発生原因から解決策の技術的詳細まで、PRの内容に基づいて正確かつ論理的に説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコードによって裏付けられており、ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号、HTTPステータスコード、クラス名などの数値や固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題を的確に要約しており、内容との一貫性が保たれています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に限定されており、裏付けのない外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間に関する表現は事実に基づいており、PRの内容を歪めるような記述はありません。