1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RailsのRackミドルウェアについて調べてみた

Posted at

RailsアプリケーションでRack::Multipart::MultipartTotalPartLimitErrorというエラーに遭遇し、なぜこうなるのか理解できていませんでした。

クライアントからリクエストを受けてからRailsがレスポンスを返す流れを整理してみました。

Rack::Multipart::MultipartTotalPartLimitError

Rackがマルチパートリクエスト(主にファイルアップロードや複数のフォームパラメータを含むPOSTリクエスト)を処理する際に、「受信したパート(フォームフィールドやファイルなど)の合計数」が設定された上限(デフォルト4096)を超えた場合に発生する。

multiparts(マルチパート)とは、1つのリクエストやメッセージの中で「複数のデータ(パート)」をまとめて送信できる仕組みです。
Webの世界では、特にmultipart/form-dataという形式がHTMLフォームからファイルやテキストデータを一緒に送るために使われます。

引用元: https://zenn.dev/iizukasan/scraps/1f4823d4b39099

受信したパート(multipart/form-data)が4096を超えるPOSTリクエストの場合に起きるエラーのようです。

これは、悪意のあるユーザーが、何万もの非常に小さなパーツを持つリクエストをサーバーに送りつけると、サーバーはその解析に大量のCPUとメモリを消費してしまいます。これにより、サーバーがダウンしたり、他のユーザーへの応答が極端に遅くなったりする DoS攻撃(サービス妨害攻撃) につながる可能性があります。

この攻撃を防ぐために、Rackは受け付けるパーツの総数に上限(デフォルトでは4096)を設けており、それを超えるとこのエラーを発生させてリクエストを拒否するようになっています。

エラーが発生するタイミング

  1. Nginx / Puma
    ユーザーからのリクエスト(multipart/form-data形式のデータを含む)をそのまま受け取ります。この時点では、中身が何個のパーツに分かれているかはまだ解析していません。

  2. Rack Middleware Stack
    リクエストはRails本体に届く前に、いくつかのミドルウェアを通過します。その中の一つに、ファイルアップロードなどを担当するRack::Multipartがあります。

  3. Rack::Multipart がリクエストを解析 ← ★エラーはここで発生
    Rack::Multipartは、送られてきたデータをRailsが扱いやすいようにパーツごとに分解・整理します。この分解作業の開始直後に、「パーツが全部で何個あるか」をチェックします。
    この時点で、パーツ数が上限(例: 4096個)を超えていると判断されると、即座に処理を中断し Rack::Multipart::MultipartTotalPartLimitErrorを発生させます。

    【処理がここでストップするため、これ以降には進まない】

  4. Rails Router

  5. Rails Controller

レストランを例に役割の整理

コンポーネント 役割 (レストランの例え) 主な仕事
🤵 Nginx 受付・案内係 全リクエストの受付、静的ファイルの高速配信、Pumaへのリクエスト転送(リバースプロキシ)
👨‍💼 Puma/Unicorn フロアマネージャー Railsアプリケーションの管理、NginxからのリクエストをRailsに渡す
📝 Rack 共通の注文票 PumaとRailsが会話するための「ルール・規格」。コンポーネントではなく約束事。
👨‍🍳 Rails キッチン・シェフ データベースとの連携、ビジネスロジックの実行、最終的なWebページ(HTML)の生成

リクエストの流れ

Rack::Multipart::MultipartTotalPartLimitErrorへの対応

フロント側でバリデーション

今回のケースだとRailsのバリデーションの前にエラーが起きるので、フロント側にバリデーションをするのが良さそうです。
パーツ総数上限(デフォルトでは4096)を超える場合にメッセージを表示したいので、3,000 ~ 4,000のパーツを含むリクエストに対してバリデーションするようにできると良さそうです。

バックエンド側でバリデーション

フロント側でバリデーションをしても悪意あるリクエストや直接APIを叩くことも可能です。
そしてシステムとしての独立性と安全性を保つために、バックエンド側にもバリデーションがあると良さそうです。
(*RackのRACK_MULTIPART_TOTAL_PART_LIMITは無制限にすることも可能。将来的に無制限にする対応などがあれば、悪意あるリクエストが来やすくなってしまうかも)

バリデーションをそれぞれのモデルに追加する工数が大きい場合は、まずレスキューさせておくのも手になりそうです。

バックエンド側でレスキュー

(推測)
Rackミドルウェアでエラーになっても、バックエンド側でレスキューすることでクライアントにメッセージを返すことも可能そうです。

class ApplicationController < ActionController::API
  rescue_from Rack::Multipart::MultipartTotalPartLimitError, with: :render_payload_too_large

  private

  def render_payload_too_large
    render json: {
      error: "Payload Too Large",
      message: "一度に送信できる項目数が上限を超えています。"
    }, status: :payload_too_large
  end
end

まとめ

クライアントからリクエストされ、Railsに届く前にエラーが起きていた事象について整理してみました。

現在のシステムの状況を整理して、バリデーションを設けることでクライアントが使いやすいサービスになりそうです!

誤りやより良いバリデーション、対応方法があれば教えてください 🙌

References

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?