WebSurfer's Home

Filter by APML

ASP.NET Core で 64KB 以上のファイルアップロード失敗

by WebSurfer 7. November 2025 13:44

元の話は Microsoft Q&A のスレッド ASP .Net Core 6 / IIS Server File Upload Restriction ? のものです。Windows Server の IIS 10 でホストされる ASP.NET Core Web アプリのファイルアップロードで、ファイルサイズが 64KB より小さい場合は問題ないが、それを超えるとアップロードに失敗するという話です。

ファイルアップロード

なお、Microsoft Q&A の質問者さんのコードは、サイズが 64KB より大きいファイルのアップロードを含め、期待通り動くことは自分の環境でですが確認しました。multipart/form-data 形式で CSRF トークンと input type="file" で選んだファイルが送信され、サーバー側で OnPost メソッドの引数 IList<IFormFile> files にバインドされます。

たった 64KB 超でアップロードに失敗するということが一般的に起きているとすると FAQ 的な話になるはずですが、過去にそのような話は聞いたことがないので、多分 Microsoft Q&A の質問者さんの環境独自の問題だと思われます。なので、一般的には参考にならない話かもしれませんが、調べたことを備忘録として書いておきます。

Microsoft のドキュメント「ASP.NET Core でファイルをアップロードする」の「ファイル アップロードのシナリオ」のセクションに、

"1 つで 64 KB を超えるバッファーファイルは、メモリからディスク上の一時ファイルに移動されます。より大きな要求の一時ファイルは、ASPNETCORE_TEMP 環境変数で指定された場所に書き込まれます。 ASPNETCORE_TEMP が定義されていない場合、ファイルは現在のユーザーの一時フォルダーに書き込まれます。"

・・・と書いてあります。

「メモリからディスク上の一時ファイルに移動」するということは、メモリのデータをファイルとして特定のフォルダーに書き込むということになります。書き込むには、それを行うプロセスのアカウントが書き込み先のフォルダーに対して書き込み権限を持っていなければなりません。

Microsoft Q&A のスレッドの質問者さんのケースでは、しきい値 64KB を超えるファイルがアップロードされた際、ASP.NET がそれをメモリからディスク上に一時ファイルとして移動しようとしたが、移動み先のフォルダーに書き込み権限が無くて失敗したということだと思われます。

しきい値を超えなければメモリにバッファされたままになりますので、ファイルサイズが 64KB より小さい場合は問題なかったということのようです。

しきい値 64KB は FormOptions.MemoryBufferThreshold プロパティで設定されています。デフォルトは 64KB ですが変更は可能です。

質問者さんの環境で、Program.cs に以下の設定を追加して、しきい値を 128KB に変更したらアップロードに成功したということですので、やはり原因は移動先のフォルダーに書き込み権限が無かったということだったと思われます。

services.Configure<FormOptions>(options =>
{
    options.MemoryBufferThreshold = 131072; // 128 KB
});

上の方法でしきい値を増やすのは、応急処置であればともかく、恒久処置として適切かはよく検討した方が良さそうです。

なぜなら、同時に多数のユーザーがファイルをアップロードするようなサイトでは、MemoryBufferThreshold を増やすとメモリが分断されてすぐにメモリ不足になってしまう恐れがあるからです。恒久処置としてはアクセス権の問題を解決してディスク上に一時ファイルとして移動できるようにするのが良さそうです。

逆に、めったに大きなファイルのアップロードはしないサイトなら (例えば、個人のブログのようなサイト)、ディスクにバッファリングされないように MemoryBufferThreshold の設定値を大きくしておく方が良いかもしれません。

では、アクセス権の問題を解決してディスク上に一時ファイルとして移動できるようにするにはどうするかですが、基本的には IIS のワーカープロセスのアカウントに移動先のフォルダーに対する書き込み・読み出し権限を与えるということになります。

移動先のフォルダーがどこにあるかですが、ASPNETCORE_TEMP 環境変数で指定されている場合はそこになります。ASPNETCORE_TEMP 環境変数の設定がない場合は (Windows Server ではデフォルトではその設定はないそうです)、現在のユーザーの一時フォルダーになります。

現在のユーザーの一時フォルダーはどこにあるかと言うと、Windows OS では %temp% で示されるフォルダーです。具体的には、IIS のワーカープロセスのアカウント(アプリケーションプールの Identity)がユーザープロファイルを持っている場合は以下のフォルダーとなります。

C:\Users\<ユーザー名>\AppData\Local\Temp

例えば、IIS Manager を使って AspNetCoreCookieAuth と言う名前のアプリケーションプールを作成したとします。その詳細設定は以下のようになります。

アプリケーションプールの詳細設定

名前が AspNetCoreCookieAuth、プロセスモデルの ID が ApplicationPoolIdentity、ユーザープロファイルの読み込みが True になっているとことに注目してください。デフォルトでそのようになるはずです。

このアプリケーションプールで ASP.NET Core アプリを動かすとアプリケーションプール名で C:\Users フォルダ下に以下の画像の通り AspNetCoreCookieAuth と言う名前のフォルダが生成されます。

Temp

%temp% は C:\Users\AspNetCoreCookieAuth\AppData\Local\Temp になりますが、そのフォルダも生成されています。

フォルダ AspNetCoreCookieAuth のプロパティを開いてセキュリティタブを見ると、ユーザー AspNetCoreCookieAuth(アプリケーションプールの Identity)にフルコントロール権限が与えられているところに注目してください。

なので、この状態(デフォルト)であれば、移動先のフォルダーに対する書き込み・読み出し権限は既に与えられているので、何もする必要はありません。それゆえ、たった 64KB 超でアップロードに失敗するということは自分は初耳だったのだと思います。

アプリケーションプールの Identity がユーザープロファイルを持たないケースもあるそうです。それはアプリケーションプールを作成する際プロセスモデルの ID に (1) ユーザープロファイルを持たないカスタムアカウントを使った、(2) LocalSystem, NetworkServce などを使った、(3) ApplicationPoolIdentity を使ったが詳細設定で[ユーザープロファイルの読み込み]を False に設定した場合です。

その場合、%temp% は C:\Windows\Temp になるそうです (未検証・未確認)。 自分の環境でそのフォルダのプロパティを開いてセキュリティを見ると以下のようになっています。

C:\Windows\Temp のセキュリティ

IIS_IUSRS は IIS のワーカープロセスのアカウントが属するグループ、Users は IUSR が属するグループですが、書き込み・読み出し権限は設定されてません。したがって、このフォルダーに書き込みに行けば権限がないので失敗するということになります。

想像ですが、Microsoft Q&A の質問者さんのケースで 64KB を超えるファイルアップロードに失敗したのは、アプリケーションプールの Identity がユーザープロファイルを持たないからではなかろうかと思います。

最後にもう一つ。ASP.NET Core アプリを IIS でホストする場合、インプロセスとアウトプロセスという方法があります。

インプロセス ホスティング モデル

インプロセス ホスティング モデル

アウトプロセス ホスティング モデル

アウトプロセス ホスティング モデル

いずれの場合でも、アプリケーションプールの Identity が ASPNETCORE_TEMP またはユーザーの一時フォルダーに対する書き込み・読み取り権限を持っていなければならないのは同じです。(アウトプロセスホスティングモデルの場合、w3wp.exe で動く ASP.NET Core Module が donet.exe を起動するので)

Tags: , ,

Upload Download

Web API に Blob をアップロード

by WebSurfer 4. February 2025 12:28

ASP.NET Core Web API で、ブラウザから fetch や axios を使って multipart/form-data 形式でアップロードされてきた Blob データをどのように取得できるかということを書きます。

axios のドキュメント Multipart Bodies のサンプルコード(抜粋下記)を見て、そこに書いてある Blob データを Web API でどのように取得できるかを考えたのがきっかけです。

const form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Blob([1,2,3]));
form.append('my_file', fileInput.files[0]);

上のコードの FormData を、ブラウザから JavaScript の fetch や axios を使って multipart/form-data 形式で送信すると、ボディ部分は下のようになります (Fiddler によるキャプチャ画像)。2 つ目の name が my_buffer となっているパートが Blob データです。

Fiddler によるキャプチャ画像

上のように送信されてきた my_buffer の Blob データは、ASP.NET Core Web API のアクションメソッドの引数の型を IFormFile 型とすれば取得できました。

具体的には、Web API プロジェクトで以下のクラスを定義し、

public class Blob
{
    public string? My_field { get; set; }
    public IFormFile? My_buffer { get; set; }
    public IFormFile? My_file { get; set; }
}

それをアクションメソッドの引数に設定して、JavaScript の fetch や axios を使って FormData をアクションメソッドに POST 送信すれば、下の画像の通り My_buffer プロパティに Blob を取得できます。

My_buffer プロパティに Blob を取得

MDN のドキュメント Blob に、

"File インターフェイスは Blob をベースにしており、 Blob の機能を継承してユーザーのシステム上のファイルをサポートするように拡張しています"

・・・と書いてあるとおり、input type="file" を使ってのファイルと同様の扱いになるということのようです。

最初、バイト配列として取得できるのではと思って、上の Blob クラスの My_buffer プロパティの型を byte[]? として試してみたのですが、バインドできないようで null になってしまいます。

My_buffer プロパティが byte[]? 型の場合

以下に検証に使ったコードを載せておきます。Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 9.0 で作成した ASP.NET Core Web API アプリです。

Controller

using Microsoft.AspNetCore.Mvc;
using WebApi.Models;

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        [HttpPost("blob")]
        public async Task<IActionResult> ReceiveBlob([FromForm] Blob model)
        {
            string result = $"My_field: {model.My_field}";

            if (model.My_file != null && model.My_file.Length > 0)
            {
                string filename = Path.GetFileName(model.My_file.FileName);
                result += $", My_file: {filename}";
            }

            if (model.My_buffer != null && model.My_buffer.Length > 0)
            {
                string array = string.Empty;
                using (var stream = new MemoryStream())
                {
                    await model.My_buffer.CopyToAsync(stream);
                    byte[] bytes = stream.ToArray();
                    foreach (byte b in bytes)
                    {
                        array += $"[{b:x2}]";
                    }
                }
                result += $", My_buffer: {array}";
            }

            return Content(result);
        }
    }
}

View

@{
    ViewData["Title"] = "SendBlob";
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewData["Title"] - WebApi</title>
    <script type="text/javascript">
        const url = "/api/upload/blob";

        const upload = async () => {
            const fileInput = document.getElementById("fileupload");
            const resultDiv = document.getElementById("result");
            const form = new FormData();
            form.append('my_field', 'my value');
            form.append('my_buffer', new Blob([1, 2, 3]));
            form.append('my_file', fileInput.files[0]);

            const param = {
                method: "POST",
                body: form
            }
            const response = await fetch(url, param);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        };

        window.addEventListener('DOMContentLoaded', () => {
            const btn = document.getElementById("button1");
            btn.addEventListener("click", upload);
        });
    </script>
</head>
<body>
    <input type="file" name="fileupload" id="fileupload" multiple="multiple" />
    <br />
    <button type="button" id="button1">Upload</button>
    <br />
    <div id="result"></div>
</body>
</html>

上の View のコードの実行結果は以下のようになります。

View のコードの実行結果

(メモ: プロジェクトは VS2022 AspNet9 WebApi)

Tags: , , ,

Upload Download

Blazor Web App で HttpClient を使ってファイルアップロード

by WebSurfer 2. January 2025 14:42

Blazor Web App から HttpCLient を使ってファイルアップロードする方法を書きます。ベースとしたのは Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 9.0 に、 [Interactive render mode] を [WebAssembly] に、 [Interactive location] を [Per page/component] に設定して作成したプロジェクトです。設定が異なる場合は下の説明の中には当てはまらない事がありますので注意してください。

Blazor Web App で HttpClient を使ってファイルアップロード

JavaScript の fetch を使ってアップロードする例は先の記事「Blazor Web App でファイルアップロード」に書きました。それに比べて、HttpClient を使う場合は、Program.cs での HttpClient の登録が必要なこと、本来ブラウザでは使えない HttpClient を使うことの違和感などが気になりました。それでも Blazor では JavaScript より HttpClient を使うのが本筋のようです。

この記事の例では、ファイルのアップロード先は外部 Web API または本 Blazor アプリのプロジェクト内に実装した Web API としました。その Web API のアクションメソッドに Blazor Web App の Razor コンポーネントから HttpClient を使ってファイルを送信します。(実際にはブラウザから送信されますので、HttpClient から生成された WebAssembly が送信しているのだろうと思いますが、具体的にどうなっているのかは分かりません)

Visual Studio でプロジェクトを作成すると、下のソリューションエクスプローラーの画像の構成となります。青枠と赤枠の中のフォルダ / ファイルはこの記事を書く際に追加したものです。青枠がプロジェクト内に作成した Web API とファイル保存用のフォルダ、赤枠がファイルをアップロードする Razor コンポーネントです。

プロジェクトの構成

この記事の例は、Microsoft のドキュメント「クライアント側のレンダリング (CSR) を使用するサーバーへのファイルのアップロード」に該当します。それを読んですべて分かれば良いのですが、自分は読んでも分からないことがありました。

なので、この記事に、Microsoft のドキュメントを読んでも分からなかったことを調べて書くとともに、実装を基本的な部分のみに簡略化したコードを備忘録として載せることにしました。

(1) InputFile クラスの使用

html の <input type="file"> を使った場合は、ユーザーが選択したファイルのデータを HttpClient の C# のコードに渡すことができません (裏ワザとかがあるかもしれませんが)。

InputFile クラスを使用すると、ユーザーがファイルの選択を完了したときに発生する OnChange イベントのハンドラの引数に InputFileChangeEventArgs オブジェクト が渡され、ユーザーが選択したファイルのデータを HttpClient の C# のコードで取得できます。

それゆえ、アップロードするのに HttpClient を使う場合は、InputFile クラスを使用するほか選択肢はなさそうです。

(2) Razor コンポーネントの配置場所

上のソリューションエクスプローラーの画像に示すように、メインプロジェクトとクライアントプロジェクト (.Client) の 2 つのプロジェクトが作られます。そのどちらにも Razor コンポーネントを配置できるのですが、今回のファイルをアップロードする Razor コンポーネントはどちらに配置すればいいのでしょうか?

答えはクライアントプロジェクト (.Client) 側です。上のソリューションエクスプローラーの画像の赤枠で示した FileUpload.razor がそれです。理由は以下の通りです。

Microsoft のドキュメント「ASP.NET Core Blazor プロジェクトの構造」に "対話型 WebAssembly または対話型自動レンダリング モードを使用するコンポーネントは、.Client プロジェクトに配置する必要があります" と書いてあります。

「対話型」の対話というのはユーザーとの対話を意味します。例えばテンプレートで自動生成されるコンポーネント Counter.razor のようにユーザーのクリックに応じてカウント値が変わっていくものが該当するようです。InputFile クラスの OnChange イベントで処理を行うのも対話に含まれるようです。

実は、最初 FileUpload.razor はメインプロジェクトに配置していたのですが、OnChange のハンドラが動かず、その解決に半日ほどハマったのは内緒です。(笑)

(3) Program.cs で HttpClient サービスの追加

HttpClient サービスの追加をメインプロジェクとクライアントプロジェクト (.Client) 両方の Program.cs で行う必要があります。

上に紹介した Microsoft のドキュメントに書いてある通り、メインプロジェクトの Program.cs に、

// IHttpClientFactory および関連するサービスを追加
builder.Services.AddHttpClient();

クライアントプロジェクト (.Client) の Program.cs に、

// Web API に対する POST 要求のための HttpClient の登録
builder.Services.AddScoped(sp =>
    new HttpClient 
    { 
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

・・・を追加します。(注: 後者のコードは「Blazor WebAssembly スタンドアロン アプリ」のテンプレートを使って作成した Blazor WebAssembly プロジェクトの Prpgram.cs には最初から含まれています)

でも、考えてみると HttpClient を使うのはクライアントプロジェクト (.Client) の FileUpload.razor なのに、なぜメインプロジェクトの Program.cs でも追加しなければならないのかが腑に落ちません。

Microsoft のドキュメントには "クライアント側コンポーネントはサーバーでプリレンダリングされるため、HttpClient サービスをメインプロジェクトに追加する必要があります。対話型コンポーネントではプリレンダリングが既定で有効になっています" と書いてありました。そう言われても、その仕組みは全く理解できていませんが。(汗)

上のクライアントプロジェクト (.Client) の Program.cs での設定例では、HttpClient の BaseAddres プロパティを設定していますがその理由を書きます。

builder.HostEnvironment.BaseAddress というのは Blazor アプリのベースアドレスで、この記事の例では https://localhost:44340/ となります。

Microsoft のドキュメント「HttpClient.BaseAddress プロパティ」に "相対 URI を使用して HttpRequestMessage を送信すると、メッセージ Uri が BaseAddress プロパティに追加され、絶対 URI が作成されます" と書かれています。

なので、Blazor アプリ内の Web API (上のソリューションエクスプローラーの画像の青枠がそれ) にファイルを送信する場合は、上のコードのように BaseAddress プロパティを設定しておくと、FileUpload.razor の HttpClient のコードで URI を指定する際、相対 URI を使うことができるようになります。

外部 Web API にファイルを送信する場合は、FileUpload.razor の HttpClient のコードで絶対 URI を指定しますが、その場合はそれが HttpClient の BaseAddress に追加されるということはなく、コードで指定した絶対 URI がそのまま使用されます (ドキュメントにはそのことは書いてないですが検証して確認)。

HttpClient のコードで常に絶対 URI を指定するのであれば上のような HttpClient の BaseAddress の設定は不要で、以下のようにすれば良いです。

// FileUpload.razor の Http.PostAsync(url, content) の url に
// http から始まる絶対 URI を指定するのであれば以下で良い
builder.Services.AddScoped(sp => new HttpClient());

(4) FileUpload.razor

ユーザーがファイルを選択してアップロードする razor コンポーネント (FileUpload.razor) のサンプルコードは以下の通りです。Blazor Web App はデフォルトでは static rendering になるそうで、@rendermode InteractiveWebAssembly を設定しないと対話型にならないので注意してください。

@page "/fileupload"
@rendermode InteractiveWebAssembly
@using System.Net.Http.Headers
@inject HttpClient Http

<h3>Upload File</h3>

<InputFile OnChange="OnInputFileChange" multiple />
<br />
<p>@result</p>

@code {
    // 外部 Web API の URI
    // private string url = "https://localhost:44366/FileUpDownload/multiple";

    // Blazor アプリのプロジェクト内に作成した Web API の URI
    // Program.cs での HttpClient の登録で BaseAddress に本 Blazor 
    // アプリのベースアドレスを設定してあるので相対 URI を使用可
    private string url = "/FileSave";

    private string result = string.Empty;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        using var content = new MultipartFormDataContent();

        // GetMultipleFiles メソッドの引数 maximumFileCount は
        // デフォルトで 10。超えると例外がスローされる
        foreach (var file in e.GetMultipleFiles())
        {
            // OpenReadStream メソッドの引数 maxAllowedSize は
            // デフォルトで 500KB。超えると例外がスローされる
            var streamContent = new StreamContent(file.OpenReadStream());
            streamContent.Headers.ContentDisposition =
                new ContentDispositionHeaderValue("form-data")
                    {
                        // Name は Web API の引数名に合わせる
                        Name = "postedfiles",  
                        FileName = Path.GetFileName(file.Name)
                    };
            streamContent.Headers.ContentType =
                new MediaTypeHeaderValue(file.ContentType);

            content.Add(streamContent);
        }

        HttpResponseMessage response = await Http.PostAsync(url, content);

        // Web API からの応答のコンテンツの文字列を取得
        result = await response.Content.ReadAsStringAsync();
    }
}

上のコードでは、ユーザーがファイルの選択を完了すると、ファイルは即 Web API に送信されます。

それを、ユーザーがファイルを選択した後でボタンクリックによりアップロードしたい場合は、OnInputFileChange メソッドでは e.GetMultipleFiles() メソッドで IReadOnlyList<IBrowserFile> オブジェクトを取得してそれを保持するに留め、ボタンクリックのハンドラで保持された IReadOnlyList<IBrowserFile> オブジェクトからファイルデータを取得して送信するのが良さそうです。

(5) Web API

FileUpload.razor から送信されたファイルを受け取って UploadedFiles フォルダに保存する Web API のサンプルコードを下に載せておきます。

この記事のコードでは Microsoft の記事「ASP.NET Core でファイルをアップロードする」に書かれたセキュリティに関する配慮はされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、アプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するようになっています。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWebAppWASM.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class FileSaveController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileSaveController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> MuilipleFiles(List<IFormFile>? postedFiles)
        {
            string result = "Uploaded files: ";
            if (postedFiles != null)
            {
                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                foreach (var postedFile in postedFiles)
                {
                    if (postedFile != null && postedFile.Length > 0)
                    {
                        // アップロードされたファイル名を取得
                        string filename = Path.GetFileName(postedFile.FileName);

                        // アプリケーションルート直下の UploadedFiles フォルダに書き込み
                        string filePath = $"{contentRootPath}\\UploadedFiles\\{filename}";
                        using (var stream = new FileStream(filePath, FileMode.Create))
                        {
                            await postedFile.CopyToAsync(stream);
                        }

                        result += $"{filename} ";
                    }
                }
            }
            else
            {
                result = "postedFiles is null";
            }

            return Content(result);
        }
    }
}

Blazor アプリのプロジェクト内に実装する場合は、上のソリューションエクスプローラーの画像の青枠に示したようにメインプロジェクトに配置します。さらに、メインプロジェクトの Program.cs に下の 2 行を追加します。

// Controller を使用できるようサービスを追加
builder.Services.AddControllers();

// ・・・中略・・・

// 属性でルーティングされたコントローラーをマップ
app.MapControllers();

外部 Web API に上と同様なコードを実装して FileUpload.razor から送信されたファイルを受け取ることもできます。ただし、その場合はクロスドメインになるので、Web API 側のサーバーが CORS 対応している必要があります。

HttpClient を使っていると言っても、結局それは WebAssembly に変換されてブラウザに送信され、ブラウザから Web API に要求が出るので、クロスドメインでの要求は、JavaScript の fetch を使った場合と同様に、CORS 対応がされてないと失敗します。

外部 Web API のサーバーで CORS 対応がされていれば、下の Fiddler でのキャプチャ画像のように #95 でプリフライトリクエストが出て、#96 でファイルが送信されます。

Fiddler でのキャプチャ画像

Tags: , , , ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  November 2025  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar