SendGrid ingress で不正な envelope を 422 で安全に処理

rails/rails

Action Mailbox の SendGrid ingress が、構文上は正しいが期待するスキーマを満たさない envelope パラメータを受け取った際に NoMethodError でクラッシュしていた問題を修正します。今回の変更により、不正な envelope は既存の JSON パースエラー時と同じ 422 Unprocessable Content で一貫して拒否されます。

背景

#57334 が報告した通り、SendGrid ingress はすでに envelope パラメータが不正な JSON であれば 422 を返す仕組みを持っていました。しかし、JSON として正しくパースできるが to キーを含まないケース(例: {"from":"jason@37signals.com"})では、コントローラが envelope["to"].each を呼び出した際に NoMethodError: undefined method 'each' for nil が発生し、未処理例外としてクラッシュしていました。

この問題は、認証済みリクエストにのみ影響します。ingress の Basic 認証を通過した後の Webhook ペイロードの検証の問題であり、認証バイパスには該当しません。既存の rescue JSON::ParserError ハンドラが不正な JSON を捕捉できていたのに対し、JSON として有効だが構造が不正なケースが考慮されていなかったという設計上のギャップが今回の修正で埋められています。

技術的な変更

MalformedEnvelopeError という専用の例外クラスを導入し、envelope のバリデーションを独立したメソッドに切り出すことで、エラー処理の一貫性を確保しました。

変更前のコードでは、envelope メソッドが JSON のパースのみを担当し、取得した値の構造検証を行っていませんでした。

変更前:

def mail
  params.require(:email).tap do |raw_email|
    envelope["to"].each { |to| raw_email.prepend("X-Original-To: ", to, "\n") } if params.key?(:envelope)
  end
end

def envelope
  JSON.parse(params.require(:envelope))
end

変更後:

class MalformedEnvelopeError < StandardError
  def initialize(message = "Malformed SendGrid envelope")
    super
  end
end

def mail
  params.require(:email).tap do |raw_email|
    envelope_recipients.each { |to| raw_email.prepend("X-Original-To: ", to, "\n") } if params.key?(:envelope)
  end
end

def envelope_recipients
  envelope = JSON.parse(params.require(:envelope))
  raise MalformedEnvelopeError unless envelope.is_a?(Hash)
  raise MalformedEnvelopeError unless envelope.key?("to")

  envelope["to"].tap do |recipients|
    raise MalformedEnvelopeError unless recipients.is_a?(Array) && recipients.all?(String)
  end
end

envelope_recipients メソッドは以下の順序でバリデーションを行います。まず envelopeHash であることを確認し、次に "to" キーの存在を確認します。最後に "to" の値が Array であり、全要素が String であることを all?(String) で検証します。

create アクションの rescue 節には MalformedEnvelopeError が追加されており、バリデーション失敗時も JSON::ParserError と同じ 422 Unprocessable Content パスを通ります。テストでは、以下の4ケースが assert_rejects_malformed_envelope ヘルパーで網羅されています:

  • to キーが存在しない envelope(例: {"from":"jason@37signals.com"}
  • Hash ではなく Array の envelope(例: ["replies@example.com"]
  • to が Array でなく String の envelope(例: {"to":"replies@example.com"}
  • to の要素が String でない envelope(例: {"to":[1]}

設計判断

既存の rescue ブロックを拡張するアプローチ が採用されました。新しいカスタム例外 MalformedEnvelopeErrorJSON::ParserError と並べて捕捉することで、「envelope が正しく処理できないケースはすべて 422 を返す」という一貫したポリシーを表現しています。

バリデーションロジックを envelope_recipients として独立したメソッドに集約したことで、コントローラの mail メソッドは受信者リストを取得するという責務に集中できます。例外を使ったバリデーションは早期リターンのチェーンに比べて各条件が明示的であり、将来的にバリデーション項目が増えた場合も同パターンで追加できます。

まとめ

本PRは、HTTP 境界での防御的なバリデーションを強化した修正です。カスタム例外クラスの導入と既存 rescue ブロックへの統合という最小限の変更で、envelope の不正な構造に起因するクラッシュを排除し、既存の 422 レスポンスポリシーを一貫して適用できるようにしています。

記事メタデータ

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

この記事は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番号のリンク記法など、全てのカスタムMarkdown構文がガイドライン通りに正しく使用されている。

対象読者への適合性 ✓ PASS

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

Action Mailboxやコントローラ、Webhookといった概念を前提としており、専門知識を持つエンジニアという対象読者に完全に適合している。冗長な説明がなく、簡潔で的確な記述がされている。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが明確。1段落1トピックの原則も守られており、非常に読みやすい。

Diff内容との照合 ✓ PASS

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

変更前後のコード引用はDiffの内容を正確に反映しており、ファイルパスも正しい。Diffから本質的な変更点を的確に抽出し、比較・解説している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「SendGrid ingress」「envelope」「NoMethodError」「422 Unprocessable Content」など、関連する技術用語を正確かつ適切に使用している。

説明の技術的正確性 ✓ PASS

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

`MalformedEnvelopeError`の導入目的や、`envelope_recipients`メソッドにおけるバリデーションロジックの説明が、実際のコードと完全に一致しており、技術的に正確である。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題点、修正内容、テストケースなど)は、PRのDescription、Diff、関連Issueで完全に裏付けられている。ハルシネーションは一切見られない。

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

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

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

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

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

記事のタイトルは、元のPR「Handle invalid SendGrid envelope input in Action Mailbox」の内容を、より具体的に「422で安全に処理する」という解決策まで含めて表現しており、非常に的確である。

外部知識の正確性 ✓ PASS

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

PR情報に記載のない外部知識(LTS情報、リリース日程等)の持ち込みはなく、提供された情報源の範囲内で記事が構成されている。

時間表現の正確性 ✓ PASS

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

「...クラッシュしていた問題」「...仕組みを持っていました」など、過去の状態と今回の修正内容に関する時間表現が正確に使われている。