ASP.NET Core を IIS 配下で動かした途端、ローカルでは通っていた大きめファイルのアップロードが「413 Payload Too Large」で落ちる――しかも「特定の API だけサイズを緩めたい」のに、IIS 全体設定は触りたくない。この記事では、そのジレンマを整理しつつ、現実的に取れる選択肢と具体的な web.config/コード例をまとめます。
IIS 配下の ASP.NET Core で 413 Payload Too Large が出る理由
まず押さえておきたいのは、「IIS のリクエストサイズ制限は ASP.NET Core のコードより前で評価される」という点です。 つまり、アクションに [RequestSizeLimit] を付けても、IIS 側でブロックされたリクエストはそもそもアプリに届きません。
Kestrel ローカル開発では問題が出ない理由
ローカル開発で dotnet run しているときは、多くのケースで Kestrel を単体で使っています。この場合、
IISServerOptions.MaxRequestBodySizeなどの IIS 向けオプションは関係なし- Kestrel の
MaxRequestBodySizeや[RequestSizeLimit]だけで制御できる
ので、MaxRequestBodySize = null; などと設定しておけば、大きなリクエストでも普通に通ってしまいます。
IIS 経由だと 413 になる理由
IIS 配下(in-process / out-of-process どちらでも)では、概ね次のようなレイヤでサイズがチェックされます。
- HTTP.sys(OS カーネル)
- IIS の Request Filtering(
requestFiltering/requestLimits) - ASP.NET Core モジュール(ANCM)
- Kestrel/ASP.NET Core ミドルウェア・MVC
このうち 2 の Request Filtering の maxAllowedContentLength が既定で 30,000,000 バイト(約 28.6MB)に設定されており、それを超えると IIS 自身が 413(または 404.13)を返します。 この時点でリクエストは ASP.NET Core に届いていないため、コントローラ側でいくらサイズ制限を緩めても意味がありません。
IIS と ASP.NET Core のサイズ制限を整理する
よく混同されるので、「どのレイヤで何を見ているか」を表に整理しておきます。
| レイヤ | 設定項目 | 既定値(概略) | 単位 | 備考 |
|---|---|---|---|---|
| IIS Request Filtering | requestLimits@maxAllowedContentLength | 30,000,000 | バイト | 超えると IIS が 413/404.13 を返す。サイト/アプリ/ディレクトリ単位で設定可能。 |
| IIS serverRuntime | uploadReadAheadSize | ~ 48KB 前後 | バイト | ヘッダー+少量の本文を先読みするサイズ。大きい multipart/form-data で 413/404.13 の一因になることがある。 |
| Kestrel / ASP.NET Core | MaxRequestBodySize(Kestrel)、[RequestSizeLimit] 属性 | 約 30MB | バイト | IIS を通過した後に評価される。エンドポイント単位で上書き可能。 |
| フォームバインド | FormOptions.MultipartBodyLengthLimit | 約 128MB | バイト | IFormFile 受信時に使用。ファイルアップロード API では明示設定推奨。 |
ポイントは、「IIS の maxAllowedContentLength が、ASP.NET Core の MaxRequestBodySize よりも先に評価される」こと。Microsoft の in-process hosting ドキュメントにも、IIS の上限は IISServerOptions.MaxRequestBodySize より前に処理されると明記されています。
グローバル設定を触らずにサイト単位で上限を緩める
「サーバ全体の設定(applicationHost.config)は変更できないが、あるサイトだけ許可サイズを広げたい」というケースでは、該当サイトのルートに置かれる web.config に requestFiltering を追加します。
サイトルートの web.config の例:
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!-- 例: 500MB を許可(値はバイト) -->
<requestLimits maxAllowedContentLength="524288000" />
</requestFiltering>
</security>
<!-- 必要に応じて uploadReadAheadSize も -->
<!-- <serverRuntime uploadReadAheadSize="1048576" /> -->
</system.webServer>
</configuration>
この設定は「そのサイトの配下にだけ」有効で、他サイトやサーバ全体の設定には影響しません。IIS マネージャーで該当サイトだけ Request Filtering → “Edit Feature Settings…” を開くと、同様の設定が GUI からも確認できます。
ただし、IIS 管理側で requestFiltering がロックされていると、サイトの web.config で上書きできない場合があります。その場合は、IIS 管理者にロック解除(overrideModeDefault など)を依頼する必要があります。
よく使うサイズのバイト数早見表
設定値はバイト単位なので、よく使うサイズはあらかじめ把握しておくと便利です。
| 目標サイズ | 計算 | 設定値(バイト) |
|---|---|---|
| 50MB | 50 × 1024 × 1024 | 52,428,800 |
| 100MB | 100 × 1024 × 1024 | 104,857,600 |
| 200MB | 200 × 1024 × 1024 | 209,715,200 |
| 500MB | 500 × 1024 × 1024 | 524,288,000 |
「特定エンドポイントだけ」サイズを広げる現実的なパターン
本題の「あるアップロード API だけサイズを緩めたい」ケースに戻ります。IIS の仕様上、「コントローラ単位で maxAllowedContentLength を変える」といったことは直接はできません。そこで現実的な選択肢として、次の 3 パターンを考えます。
- パターン A:
<location>要素で URL パスを絞る - パターン B: アップロード専用の「子アプリ(仮想アプリケーション)」として分離
- パターン C: 別サイト/サブドメインとして完全に分離
パターン A:web.config の <location> で URL パスを限定する
IIS の構成は、サイト/アプリ/物理ディレクトリごとに分割して持つことができます。<location> 要素を使うと、「このパス以下だけ Request Filtering の設定を変える」といったことが可能です。
例えば、/api/upload だけ 500MB まで許可し、それ以外は 50MB のままにする例は次のようになります。
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!-- サイト全体の上限(例: 50MB) -->
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
</system.webServer>
<!-- /api/upload だけ 500MB まで許可 -->
<location path="api/upload">
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="524288000" />
</requestFiltering>
</security>
</system.webServer>
</location>
</configuration>
注意点として、path の解釈は「IIS 上の仮想ディレクトリ構造」に依存します。
たとえば /api/upload が ASP.NET Core のルーティングだけで実現されており、物理ディレクトリが存在しない場合、環境によっては期待通りマッチしないケースがあります。その場合は次の「子アプリ分離(パターン B)」の方が確実です。
パターン B:アップロード専用の子アプリ(仮想アプリケーション)に分離する
もっとも「堅い」方法は、アップロード用エンドポイントを IIS 上で別アプリケーションとして切り出し、そのアプリだけ maxAllowedContentLength を大きくするやり方です。
構成イメージ:
- ルートサイト:
https://example.com/→ 通常の Web アプリ(最大 50MB) - 子アプリ:
https://example.com/upload/→ アップロード専用 API(最大 500MB)
IIS 上のディレクトリ構造の例:
C:\inetpub\wwwroot\MySite
├─ web.config (通常サイト用、50MB)
├─ MySite.dll
└─ upload\
├─ web.config (アップロード専用、500MB)
└─ UploadApi.dll
IIS マネージャーでの作業フローは次の通りです。
- サイトの物理パス配下に
uploadフォルダーを作成 - IIS マネージャーで該当サイトを開き、
uploadフォルダーを右クリック → 「変換してアプリケーションへ」 - アプリケーション プール(必要なら専用)を指定し、エイリアスを
/uploadとして登録 - 子アプリの物理フォルダーに配置した ASP.NET Core アプリに、アップロード API を実装
- 子アプリの
web.configにのみ大きめのmaxAllowedContentLengthを設定
子アプリ側の web.config の例(500MB):
<configuration>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="524288000" />
</requestFiltering>
</security>
</system.webServer>
</configuration>
この方法だと「アップロード系だけ別アプリ」として運用できるため、
- ログ/監視/スケール戦略を切り分けやすい
- WAF や IP 制限など、セキュリティポリシーを別設定にできる
- アプリをマイクロサービス的に分割していく足がかりになる
といったメリットがあります。その代わり、デプロイ対象が一つ増える、API 間通信(トークン共有など)を考える必要がある、といったコストも発生します。
| パターン | メリット | デメリット |
|---|---|---|
| A: <location> でパス限定 | 構成がシンプル。既存アプリを分割しなくてよい。 | ルーティング構成によっては効かない場合がある。テスト必須。 |
| B: 子アプリに分離 | アップロードだけ確実に分離できる。セキュリティ・運用ポリシーも切り分けやすい。 | アプリ/デプロイが 1 つ増える。設定もやや複雑。 |
| C: 別サイト・サブドメイン | 完全に独立運用できる。大規模化に向く。 | DNS や証明書、CORS/認証など考慮事項が増える。 |
パターン C:別サイト/サブドメインとして分離する
さらに踏み込んで、https://upload.example.com のようにサブドメイン側に完全分離してしまう方法もあります。IIS 上では別サイトとして扱われるため、web.config やアプリケーション プールも完全に独立します。
メリット・デメリットはパターン B の延長ですが、「クラウドストレージへの直アップロード API だけ別インフラで運用する」ような構成も取りやすくなります。
ASP.NET Core 側でエンドポイント単位の上限を調整する
IIS の制限をクリアしたあとは、ASP.NET Core アプリ側でも「対象エンドポイントだけ」サイズを制御しておくと安全です。代表的なやり方は、
[RequestSizeLimit]属性でコントローラ/アクション単位のリクエストサイズを指定[RequestFormLimits]属性やFormOptionsでフォーム/マルチパートの制限を調整
などです。
アップロード API に付ける典型的な属性
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Features;
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
[HttpPost("files")]
[RequestSizeLimit(500_000_000)] // 500MB
[RequestFormLimits(MultipartBodyLengthLimit = 500_000_000)]
public async Task<IActionResult> Upload(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
return BadRequest("ファイルが選択されていません。");
}
long totalSize = files.Sum(f => f.Length);
if (totalSize == 0)
{
return BadRequest("空のファイルはアップロードできません。");
}
foreach (var file in files)
{
// TODO: 保存先の検証、ウイルススキャンなど
var filePath = Path.Combine("uploads", Path.GetRandomFileName());
using var stream = System.IO.File.Create(filePath);
await file.CopyToAsync(stream);
}
return Ok(new { Count = files.Count, TotalSize = totalSize });
}
}
グローバルに MaxRequestBodySize を変えたい場合は、Kestrel のオプションや IHttpMaxRequestBodySizeFeature を使ってコード側から設定できますが、IIS の maxAllowedContentLength はこれらよりも先に評価されるため、IIS 上限を超えるサイズには依然として対応できません。
非アップロード API に対するサイズガード用ミドルウェア
アップロード専用エンドポイント以外には「大きすぎるリクエストは受けたくない」という場合、Content-Length を見て早期に 413 を返す簡易ミドルウェアを入れておくと安心です(もちろん IIS の上限以内ですが)。
public class RequestSizeGuardMiddleware
{
private readonly RequestDelegate _next;
private readonly long _maxBytes;
public RequestSizeGuardMiddleware(RequestDelegate next, long maxBytes)
{
_next = next;
_maxBytes = maxBytes;
}
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path.Value ?? string.Empty;
// アップロード API は除外
if (!path.StartsWith("/api/upload", StringComparison.OrdinalIgnoreCase))
{
if (context.Request.ContentLength > _maxBytes)
{
context.Response.StatusCode = StatusCodes.Status413PayloadTooLarge;
await context.Response.WriteAsync("Request size is too large.");
return;
}
}
await _next(context);
}
}
このように「IIS の上限はあくまで安全側のハードリミット」とし、実際の業務ポリシー(通常 API は 1MB まで、など)はアプリ側で細かく制御するのがおすすめです。
uploadReadAheadSize など IIS 側で併せて見直したい設定
大きなファイルを TLS/クライアント証明書付きで送るケースなどでは、maxAllowedContentLength とは別に serverRuntime@uploadReadAheadSize の既定値がボトルネックになり、413 または 404.13 エラーを引き起こすことがあります。
必要に応じて、サイトの web.config に次のような設定を加えます。
<configuration>
<system.webServer>
<serverRuntime uploadReadAheadSize="1048576" /> <!-- 1MB -->
</system.webServer>
</configuration>
uploadReadAheadSize は「先読みする最大サイズ」であり、あまり極端に大きくしすぎるとメモリ負荷が上がる点に注意してください。まずは 256KB〜1MB など、必要最低限から調整するのが無難です。
大容量ファイルアップロードの設計ベストプラクティス
IIS/ASP.NET Core の設定だけで無理やり通すこともできますが、サイズが数百 MB〜GB クラスになってくると、そもそもの設計を見直した方が安全です。ここでは代表的な選択肢を挙げます。
クラウドストレージへの直アップロード
- サーバ側は、Azure Blob Storage / Amazon S3 などの「署名付き URL」を返すだけ
- クライアントはその URL に直接 PUT/POST でアップロード
- ASP.NET Core アプリにはメタデータ(ファイル名・サイズ・ハッシュなど)だけ通知
これにより、アプリサーバの帯域やメモリ消費を抑えられ、IIS や Kestrel のサイズ制限に縛られにくくなります。
レジューム可能な分割アップロード(Tus など)
- クライアントがファイルをチャンク(例:5MB ごと)に分割して送信
- 途中で回線が切れても、続きから再開できる
- サーバ側もチャンク単位で保存していくため、タイムアウトや再送のコストを抑えられる
ASP.NET Core でも Tus プロトコル対応ライブラリなどが存在するので、「巨大な動画ファイルを頻繁に扱う」ようなサービスでは最初から検討しておくと運用が楽になります。
セキュリティとリソース保護
大容量アップロードは、攻撃ベクトルとしても魅力的なターゲットです。次のような観点で対策を入れておきましょう。
- 認証・認可が通っているユーザーだけアップロードを許可する
- 1 ユーザーあたりの同時アップロード数・合計容量にレート制限をかける
- アップロード後にウイルススキャンや MIME タイプ検証を行う
- ストレージのクォータを設け、肥大化を防ぐ
サンプル:最小構成で「1 つの API だけ 500MB」を許可する
ここまでの内容を踏まえ、現実的で最小限な構成例をまとめます。
前提
- IIS 上に 1 サイトだけ(
https://example.com) - ASP.NET Core アプリには
/api/uploadというアップロード API が 1 つ - アップロード API だけ 500MB、他の API は 50MB を上限にしたい
web.config の例(サイト全体+location)
<configuration>
<system.webServer>
<security>
<requestFiltering>
<!-- サイト全体の上限: 50MB -->
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
<!-- 必要に応じて先読みサイズを調整 -->
<!-- <serverRuntime uploadReadAheadSize="262144" /> -->
</system.webServer>
<!-- /api/upload だけ 500MB を許可 -->
<location path="api/upload">
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="524288000" />
</requestFiltering>
</security>
</system.webServer>
</location>
</configuration>
もしこの <location> がルーティング構成上うまく効かない場合は、同じ考え方で「/upload 子アプリ」を作り、そこに API を移す(パターン B)形に切り替えれば OK です。
ASP.NET Core 側の Upload アクション例
ASP.NET Core は、IIS を通過したリクエストに対してのみ動作するため、アップロード API 側でも適切な上限を設定しておきます。
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
[HttpPost]
[Route("files")]
[RequestSizeLimit(500_000_000)]
[RequestFormLimits(MultipartBodyLengthLimit = 500_000_000)]
public async Task<IActionResult> Upload(List<IFormFile> files, CancellationToken cancellationToken)
{
if (files == null || files.Count == 0)
{
return BadRequest("ファイルが選択されていません。");
}
foreach (var file in files)
{
if (file.Length == 0)
{
continue;
}
var fileName = Path.GetRandomFileName();
var filePath = Path.Combine("uploads", fileName);
Directory.CreateDirectory("uploads");
await using var stream = System.IO.File.Create(filePath);
await file.CopyToAsync(stream, cancellationToken);
}
return Ok(new { Count = files.Count });
}
}
ここまでを整えておくと、
- 通常の API → IIS の 50MB 制限+ミドルウェアでガード
/api/upload→ IIS と ASP.NET Core の両方で 500MB まで許可
という構成が実現できます。
トラブルシューティング:413 が出たときのチェックリスト
最後に、実際に 413 が出たときに確認したいポイントをチェックリスト形式でまとめます。
- どのコンポーネントが 413 を返しているか?
IIS が返しているのか、アプリ側のミドルウェア/コントローラが返しているのかをログから切り分けます。 - IIS の Request Filtering がロックされていないか?
サーバ全体のapplicationHost.configでrequestFilteringがロックされていると、サイト単位のweb.configで上書きできません。 maxAllowedContentLengthが想定通りの値か?
GUI だけでなく、実際のweb.configを開いて値を確認します。デプロイ時に上書きされていないかもチェック。uploadReadAheadSizeが極端に小さくないか?
特に HTTPS/クライアント証明書使用時は、この値がボトルネックになることがあります。- 中間プロキシ/ロードバランサの制限はないか?
IIS の前段にリバースプロキシやクラウド LB がある場合、それぞれの最大サイズ設定も確認します。 - ASP.NET Core 側の
[RequestSizeLimit]やフォーム制限は適切か?
狙ったエンドポイントに属性が付いているか、Controller/Action 単位の指定が競合していないかを確認します。
IIS 配下の ASP.NET Core で「特定エンドポイントだけ」アップロードサイズを緩めるには、どうしても IIS の構成レベルをまたいだ設計が必要になります。 しかし、サイト単位/子アプリ単位でうまくスコープを切れば、グローバル設定を汚さずに、現場の要件に合った柔軟なアップロード API を実現できます。

コメント