Mandrill ingressが`raw_msg`欠落イベントを422で正しく拒否するよう修正

rails/rails

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_differenceActionMailbox::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への正常な拒否応答へと変えています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
a9024c69

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)という理想的な3部構成が明確に適用されています。各セクションがそれぞれの役割を適切に果たしており、非常に理解しやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```ruby:ファイルパス)とGitHubリンク記法([#123])が、ガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

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

「ingress」「ペイロード」「rescue節」などの技術用語を前提として使用しており、専門知識を持つエンジニアという対象読者に適切にフォーカスされています。

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

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

各セクションが「総論→各論」で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。1段落1トピックや適切な段落長も守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

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

記事内で引用されている変更前後のコードブロックは、提供されたDiff情報と完全に一致しています。ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「MalformedEventsError」「422 Unprocessable Content」「TypeError」など、PRやRailsの文脈における技術用語を正確に使用しています。

説明の技術的正確性 ✓ PASS

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

「raw_msg欠落によりnilが渡されTypeErrorが発生していた」という問題の原因から、「型チェックを追加し既存のエラーで処理する」という解決策まで、技術的な説明はPR情報とDiffによって裏付けられており、正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(500エラーの発生、422への修正、既存エラーの再利用など)は、PRのDescriptionやDiffの内容と一致しており、ハルシネーション(創作)は見られません。

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

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

PR番号(#57463)、Issue番号(#57462)、HTTPステータスコード(422, 500)など、記事に含まれるすべての数値・固有名詞は正確です。

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

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

記事のタイトル「Mandrill ingressが`raw_msg`欠落イベントを422で正しく拒否するよう修正」は、PRの主題「Reject malformed Mandrill inbound events without raw messages」を的確に反映しています。

外部知識の正確性 ✓ PASS

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

記事の内容はすべてPR情報とDiffに基づいており、バージョン情報やリリース予定といったPR外の外部知識を含んでいません。

時間表現の正確性 ✓ PASS

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

問題が発生「していた」という過去の事象と、修正によって「〜するようになりました」という現在の状態が、PRの内容に沿って正確に表現されています。