Action Mailbox: 不正な形式の original recipient パラメータを 422 で拒否

rails/rails

Mailgun・Postmark の Action Mailbox ingress が、不正な形式の original recipient パラメータを受け取った際に TypeError で例外を発生させていた問題を修正しました。バリデーションを追加し、不正なパラメータには 422 Unprocessable Content を返すようになります。

背景

Mailgun と Postmark の Action Mailbox ingress は、オプションの original recipient パラメータをメール本文の先頭に X-Original-To ヘッダとして付与します。具体的には String#prepend を使って "X-Original-To: ", recipient 値, "\n" を raw メールに差し込む処理です。

この処理において、recipient パラメータが配列として渡された場合、String#prependTypeError: no implicit conversion of Array into String を発生させていました(#57491)。例えば Mailgun では recipient: ["replies@example.com"]、Postmark では OriginalRecipient: ["thisguy@domain.abcd"] のような配列型のパラメータが来るケースが該当します。

ドキュメント上、不正なリクエストに対しては 422 Unprocessable Content を返すことが期待されているにもかかわらず、実際にはコントローラの外で未捕捉の例外として露出していました。これはセキュリティ上の問題ではなく、エラーハンドリングの不整合です。

技術的な変更

両 ingress コントローラに、専用の例外クラスと型チェックメソッドを追加することで、不正なパラメータをコントローラ層で安全に処理するようにしました。

Mailgun 側では MalformedRecipientError クラスと recipient メソッドを追加しました。recipient メソッドは params.require(:recipient) の値が String でない場合に MalformedRecipientError を raise します。create アクションでこの例外を rescue し、422 を返します。

変更前:

def mail
  params.require("body-mime").tap do |raw_email|
    raw_email.prepend("X-Original-To: ", params.require(:recipient), "\n") if params.key?(:recipient)
  end
end

変更後:

class MalformedRecipientError < StandardError
  def initialize(message = "Malformed Mailgun recipient")
    super
  end
end

def create
  ActionMailbox::InboundEmail.create_and_extract_message_id! mail
rescue MalformedRecipientError => error
  logger.error error.message
  head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
end

def mail
  params.require("body-mime").tap do |raw_email|
    raw_email.prepend("X-Original-To: ", recipient, "\n") if params.key?(:recipient)
  end
end

def recipient
  params.require(:recipient).tap do |recipient|
    raise MalformedRecipientError unless recipient.is_a?(String)
  end
end

Postmark 側では同様に MalformedOriginalRecipientError クラスと original_recipient メソッドを追加しました。Postmark の create アクションはすでに ActionController::ParameterMissing を rescue してログ出力と 422 レスポンスを返す処理を持っていたため、rescue 節に MalformedOriginalRecipientError を追加するだけで既存のエラーハンドリングパスを再利用できました。

変更後(Postmark の rescue 節):

rescue ActionController::ParameterMissing, MalformedOriginalRecipientError => error
  logger.error <<~MESSAGE
    #{error.message}
    ...
  MESSAGE
  head ActionDispatch::Constants::UNPROCESSABLE_CONTENT
end

テストも両コントローラに追加されており、配列型パラメータを送信した際に InboundEmail が作成されず 422 が返ることを assert_no_differenceassert_response で検証しています。

設計判断

各コントローラに独立した専用例外クラスを定義する方針が採られました。共通の基底クラスではなく、MalformedRecipientErrorMalformedOriginalRecipientError という別々のクラスが用意されています。これにより、エラーメッセージに ingress 名(Mailgun / Postmark)が明示的に含まれ、ログからの問題特定が容易になります。

バリデーションロジックを mail メソッド内にインラインで書くのではなく、recipient / original_recipient という独立したメソッドに切り出している点も注目に値します。String#prepend を呼ぶ箇所は変更せず、引数として渡す値の取得処理を分離することで、バリデーション追加の影響範囲を最小限に抑えています。

Postmark の既存エラーハンドリングを再利用した点も、コードの一貫性という観点で合理的な判断です。ParameterMissingMalformedOriginalRecipientError はどちらも「リクエストパラメータの不正」という同じ意味を持ち、同じ 422 レスポンスで処理されます。

まとめ

本 PR は、既存のエラーハンドリングの「抜け穴」を塞ぐ防御的なバリデーション追加です。String#prepend に到達する前に型チェックを行い、専用例外クラスで制御フローを明確化したことで、不正なリクエストに対するレスポンスが TypeError の uncaught exception から仕様通りの 422 Unprocessable Content へと統一されました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
31a1244b

この記事は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番号・Issue番号のリンク記法ともに正しく使用されています。

対象読者への適合性 ✓ PASS

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

技術用語が適切に使用されており、専門知識を持つエンジニアという対象読者に適合した内容になっています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiffの内容を正確に反映しています。ファイル名も一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

TypeError, 422 Unprocessable Content, String#prependなど、RailsおよびHTTPの文脈における技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

不正なパラメータがTypeErrorを引き起こすメカニズムや、その解決策としての例外処理の追加など、技術的な説明は正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescription、Diff、またはテストコードで裏付けられています。「設計判断」セクションはコードから妥当な洞察を導き出しており、ハルシネーションは見られません。

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

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

PR番号(#57492)、Issue番号(#57491)、HTTPステータスコード(422)など、すべての数値・固有名詞は正確です。

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

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

記事のタイトルは、PRの「不正な形式のoriginal recipientを拒否する」という主題を的確に表現しています。

外部知識の正確性 ✓ PASS

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

PR情報に基づかない外部知識(バージョンサポート状況、リリース日程など)の記載はなく、事実に基づいた内容となっています。

時間表現の正確性 ✓ PASS

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

「発生させていた問題を修正しました」といった過去形の表現が使われており、変更が完了した事実を正確に伝えています。