ASP.NET Core 9 の MVC アプリでサーバー上の MP4 を <video> タグで再生しようとすると、HTTP 416(Requested Range Not Satisfiable)が返って再生できないことがあります。本記事では原因になりやすい静的ファイル設定を整理し、UseStaticFiles で確実に直す手順と切り分けポイントをまとめます。
ASP.NET Core 9 MVCで動画再生がHTTP 416になるときの結論:UseStaticFiles() を入れる
HTML5 の <video> は、再生開始やシーク(再生位置の移動)のために Range ヘッダー付きの部分取得(バイトレンジ) を行います。サーバー側がこのレンジ要求に正しく応答できないと、ブラウザは再生を諦めたり、今回のように HTTP 416 を受け取って停止します。
.NET Core 9(ASP.NET Core 9)の MVC で「静的ファイルの配信」を構成する際、app.MapStaticAssets(); だけに頼ると動画のレンジ要求が期待どおりに処理されないケースがあります。対策としてはシンプルで、app.UseStaticFiles(); を有効化し、動画ファイルを wwwroot 配下から配信する ことで解消することが多いです。
| 症状 | 主な原因 | 最短の対策 |
|---|---|---|
<video> で読み込むと 416 になる | 静的ファイル配信が未設定/レンジ要求を処理できない | UseStaticFiles() を追加し、wwwroot から配信 |
| 再生はできるがシークすると止まる | 206/Content-Range が返らず Range が成立していない | レスポンスヘッダーを確認し、ミドルウェア順序も見直す |
| 特定の環境やバージョンでだけ発生する | .NET 9.x の既知不具合/テンプレート差分/プロキシ影響 | .NET と Visual Studio を更新し、環境差分を潰す |
HTTP 416(Requested Range Not Satisfiable)の原因:HTML5 videoのRangeリクエスト
HTTP 416 は、ざっくり言うと「指定されたバイト範囲で返せるデータがない」という意味です。動画再生では次のような流れがよく起きます。
- ブラウザが
Range: bytes=0-のように、先頭からの部分取得を要求する - サーバーが
206 Partial ContentとContent-Rangeを返す - 必要に応じてブラウザが別の範囲(例:
bytes=1000000-)を要求し、シークやバッファリングを行う
このとき、サーバー側が Range を理解しない/ファイルサイズを正しく認識できない/ミドルウェアが途中でレスポンスを書き換える、などが起きると、レンジ処理が破綻して 416 になります。
レンジ要求が正常なときの「期待値」
| 観点 | 期待される内容(例) | 意味 |
|---|---|---|
| リクエスト | Range: bytes=0- | 先頭から(末尾まで)を部分取得したい |
| ステータス | 206 Partial Content | 部分データを返せている |
| レスポンスヘッダー | Accept-Ranges: bytes | このリソースはバイトレンジに対応 |
| レスポンスヘッダー | Content-Range: bytes 0-999/1234567 | 返した範囲と全体サイズ |
| レスポンスヘッダー | Content-Type: video/mp4 | ブラウザが動画として扱える |
MapStaticAssets() と UseStaticFiles() の違い:動画は後者が堅い
結論だけでなく、なぜ UseStaticFiles が効くのかを理解しておくと、今後の応用やトラブルシュートが一気にラクになります。
| 項目 | app.MapStaticAssets() | app.UseStaticFiles() |
|---|---|---|
| 仕組み | エンドポイント(ルーティング)として静的アセットを公開する発想 | Static File Middleware がファイルを直接配信する定番方式 |
| 対象 | 構成によってはビルド時に解決されるアセット中心になりやすい | wwwroot 配下の実ファイルを URL にマップ |
| 動画の Range 対応 | 構成やミドルウェア順序次第で期待どおりにならないことがある | レンジ要求(部分取得)に対応しやすく、実績が多い |
| 情報量 | 比較的新しいため、事例が少ないことがある | 事例が豊富で、調べながら潰しやすい |
「MapStaticAssets は全部ダメ」という話ではなく、動画のようにレンジ対応が必須で、ファイルサイズも大きくなりやすいコンテンツは UseStaticFiles で配信するのが安全、という整理です。静的アセット周りを併用したい場合でも、まず動画については UseStaticFiles に寄せるのが現実的です。
最小構成の Program.cs 例(重要:ミドルウェアの順番)
動画が 416 になるとき、意外と多いのが「ミドルウェアの順番」のミスです。静的ファイルは UseRouting() より前で処理されるのが基本です。
var builder = WebApplication.CreateBuilder(args);
// MVC を利用する場合のサービス登録
builder.Services.AddControllersWithViews();
var app = builder.Build();
// ここで静的ファイル(wwwroot 配下)を有効化:動画もここから配信される
app.UseStaticFiles();
// ルーティング・認証など
app.UseRouting();
app.UseAuthorization();
// ルート設定
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
プロジェクトによっては、次のようなミドルウェアも追加しているはずです。重要なのは「静的ファイルが先」である点です。
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
// ここが先(Static File Middleware)
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
動画ファイルの配置:wwwroot/videos と URL の対応を確認する
UseStaticFiles は既定で wwwroot を公開します。動画ファイルは次のような配置にするのが分かりやすく、トラブルが減ります。
| ファイルの置き場所(物理パス) | ブラウザからの URL | 最初に確認すること |
|---|---|---|
wwwroot/videos/sample.mp4 | /videos/sample.mp4 | URL を直打ちして 200/206 が返るか |
wwwroot/media/movie/intro.mp4 | /media/movie/intro.mp4 | 大文字小文字・拡張子を含め完全一致しているか |
ブラウザで /videos/sample.mp4 を直接開けない場合、<video> 側を疑う前に「静的ファイルとして配信できているか」を先に解消するのが近道です。
<video> タグの例(HTML)
type 属性を付けるとブラウザの判定が安定します。preload="metadata" は、最初に必要以上のダウンロードをしないための指定で、トラブルシュート時にも挙動が読みやすくなります。
<video controls preload="metadata" width="640">
<source src="/videos/sample.mp4" type="video/mp4">
お使いのブラウザは video タグに対応していません。
</video>
動画を別フォルダに置きたい場合:追加の静的ファイルディレクトリをマップする
「容量が大きいので wwwroot とは別のフォルダに置きたい」「運用上の都合で動画だけ別ディレクトリにしたい」というケースもあります。その場合は、PhysicalFileProvider を使って 追加の公開ディレクトリ をマップできます。
using Microsoft.Extensions.FileProviders;
app.UseStaticFiles(); // 既定の wwwroot
// 例:アプリ直下の "Videos" フォルダを /videos として公開する
var videosPath = Path.Combine(app.Environment.ContentRootPath, "Videos");
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(videosPath),
RequestPath = "/videos"
});
この方法は便利ですが、指定したフォルダが URL で誰でもアクセス可能 になります。認証が必要な動画には使わず、公開してよいファイルに限定するのが安全です(認証が必要なら後述の Controller 配信が向いています)。
開発者ツールで「Range が付いているか」を見る
HTTP 416 の切り分けは、ブラウザの開発者ツール(F12)で一気に進みます。
- ブラウザで対象ページを開き、F12 で開発者ツールを表示する
- 「ネットワーク」タブを開き、動画ファイル(
.mp4)のリクエストを選ぶ - リクエストヘッダーに
Rangeが付与されているか確認する(例:Range: bytes=0-) - レスポンスのステータスが
206か、少なくとも 416 以外になっているか確認する
Range が付いているのに 416 が返る場合、サーバー側のレンジ処理が壊れている可能性が高いです。逆に、Range が付いていないのに 416 の場合は、プロキシやミドルウェアがヘッダーを変形している可能性も疑います。
コマンドで確認したい場合(curl)
ローカルや検証環境で再現するなら、curl で「レンジ要求に応答できるか」を確認できます。
# 先頭の 1 バイトだけ取りに行く(Range のテスト)
curl -I -H "Range: bytes=0-1" https://example.com/videos/sample.mp4
206 と Content-Range が返れば、少なくともレンジの基本動作は通っています。
.NET / Visual Studio の更新で直るケースもある(少なくとも .NET 9.0.2 以降を推奨)
静的ファイル配信やレンジ処理は OS・ホスティング環境・フレームワークの更新の影響を受けやすい領域です。特に .NET 9.x では、同様の症状が報告され「パッチ更新で解消した」というケースがあります。
- Visual Studio を最新バージョンへ更新する
- .NET SDK / Runtime を 9.0.2 以降 など、十分にパッチが当たった版へ更新する
「設定は合っているはずなのに挙動が変わらない」「別 PC では再現しない」といったときは、まず開発環境と実行環境の .NET バージョンを揃えるのが効果的です。
追加の確認ポイント:ここで詰まる人が多い
UseStaticFiles を入れても直らない場合、次の観点を上から潰すと原因に辿り着きやすいです。
| チェック項目 | 何が起きる? | 対処の方向性 |
|---|---|---|
| ファイルパスと URL が一致しているか | 404 や別ファイルを参照して 416 になる | URL 直打ちで 200/206 を確認。大文字小文字も含め完全一致 |
MIME タイプが video/mp4 になっているか | ブラウザが動画と認識せず挙動が不安定 | レスポンスヘッダーを確認。必要なら ContentTypeProvider を追加 |
UseStaticFiles() の位置が UseRouting() より前か | 静的ファイルに到達せず MVC で処理され、Range が壊れる | ミドルウェア順序を修正 |
| レスポンス圧縮の対象に動画を入れていないか | 圧縮されたストリームで Range が成立せずエラーになることがある | video/mp4 を圧縮対象から外す(通常は不要) |
| リバースプロキシ / CDN が Range をブロックしていないか | Range が落ちて 200 になったり、変形して 416 になる | プロキシ設定(ヘッダー転送、キャッシュ、Range 許可)を確認 |
| ファイルが配信中に更新されていないか | サイズが変わり Range が不正になることがある | 配信中にファイルを更新しない/別名で差し替える |
MIME タイプが怪しいときの例(.mp4 を明示)
通常は .mp4 は既定で video/mp4 にマップされますが、環境差やカスタム設定で崩れている場合は明示できます。
using Microsoft.AspNetCore.StaticFiles;
var contentTypeProvider = new FileExtensionContentTypeProvider();
contentTypeProvider.Mappings[".mp4"] = "video/mp4";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = contentTypeProvider
});
wwwroot に置けない場合:Controller で返すなら Range を有効化する
認証が必要な動画や、公開ディレクトリに置けないファイルを配信したい場合は、MVC のアクションからファイルを返すことがあります。このときは Range 処理を有効化 しないと、ブラウザのレンジ要求にうまく応答できず、416 やシーク不可につながります。
PhysicalFile で Range を有効化する例
using Microsoft.AspNetCore.Mvc;
public class VideosController : Controller
{
[HttpGet("/secure-videos/{fileName}")]
public IActionResult Get(string fileName)
{
var filePath = Path.Combine(
Directory.GetCurrentDirectory(),
"SecureVideos",
fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
// enableRangeProcessing: true がポイント
return PhysicalFile(
filePath,
"video/mp4",
enableRangeProcessing: true);
}
}
この方式なら、認可(ログイン必須、購入者のみ視聴など)を Controller で制御しながら、ブラウザが求める Range 応答(206/Content-Range)にも対応しやすくなります。
よくある質問
MapStaticAssets() を残したままでも大丈夫?
プロジェクトの構成によっては併用できます。重要なのは「動画の配信経路が Range 要求に耐えるか」です。迷ったら、まず動画は UseStaticFiles(または Controller の Range 有効化)で安定させ、その上で他のアセット配信を整理すると失敗しにくいです。
416 ではなく 200 が返っている。これでも再生できる?
再生自体は 200 でも始まることがありますが、シークや途中再生が不安定になりがちです。<video> が Range を使っているのに 200 しか返らない場合は、どこかで Range が落ちている可能性があります。ネットワークタブで Range の有無を確認してください。
動画のファイルサイズが大きいほど起きやすい?
大きいファイルほどレンジ要求・バッファリング・キャッシュの影響を受けやすいので、設定不備が表面化しやすい傾向があります。まずは小さな MP4 で動作を安定させ、同じ手順で大きい動画へ広げると切り分けがスムーズです。
まとめ:HTTP 416 は「Range に正しく応答できていない」サイン
<video>は Range ヘッダー付きで部分取得するため、サーバー側の静的ファイル配信設定が重要app.MapStaticAssets();だけで詰まる場合は、まずapp.UseStaticFiles();を入れる- 動画は
wwwroot配下(または追加公開ディレクトリ)に配置し、URL 直打ちで 200/206 を確認する - 開発者ツールで
Rangeとレスポンス(206/Content-Range)を確認すると原因が見えやすい wwwrootに置けない場合は Controller 返却時にenableRangeProcessing: trueを使う
最終的に、UseStaticFiles に変更したことで動画配信が正常化し、HTTP 416 が解消した、という流れは非常に典型です。まずは静的ファイルの配信経路を一本化し、Range が通る状態を作ってから、必要に応じて運用(認証、プロキシ、キャッシュ)を整えていくのがおすすめです。

コメント