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

Xperia 10 V のブラウザで日本語が太字にならない

by WebSurfer 30. September 2025 16:25

今さらながらの話ですが、自分が使っている Xperia 10 V の Chrome および Edge で自分のブログを見ると、日本語は css で font-weight: bold; を指定しても太字にならないということに気が付きました。そのあたりについて少し調べたので備忘録として書いておきます。

日本語に font-weight: bold が効かない

上の画像の本文の部分の HTML ソースは以下のとおりですが、日本語の文字には font-weight: bold; も font-weight: 600; も効いていないのが分かるでしょうか?

<p style="font-weight: bold;">
  Alice was beginning to get very tired of sitting by her sister 
  on the bank, and of having nothing to do
  アリスは、姉のそばの土手に座っているのに、何もすることがないのに、
  だんだん飽きてきました。
</p>

<div style="font-weight: 600;">
  I'm heavy 私は重いです<br />
  <span style="font-weight: lighter;">I'm lighter 私は軽い</span>
</div>

理由は Android 14 以前の OS のシステムフォントには日本語の太字がないからということで、Web デザイナなら知ってて当たり前のことらしいです。(汗)

もちろん Windows 10 PC のブラウザでは問題ありません。また、Android 15 ではフォントが改善され、日本語も太字で表示できるようになったそうです (参考: Android 15で日本語フォントが大幅改善)。家電量販店に置いてあった最新の Xperia 10, Pixcel 8 で試してみましたが、日本語も太字になることは確認できました。

自分の Xperia 10 V のオリジナルの OS は Android 13 でそれを Android 15 にアップデート済みですが、フォントのアップデートには対応してないようで、上の画像の通り日本語は font-weight: bold; で太字になりません。

「何を今さら、自分が書いたブログなんだから見たらわかっただろうに」と言われそうですね。でも、数日前までは普段自分のスマホで使っている Edge では font-weight: bold; で日本語も太字で表示されていたのです。なので、気が付きませんでした。

想像ですが、以前 Edge で日本語も太字で表示されていた時は文字を重ねて擬似的に太く見せるなどの手段が取られていた。Android 15 でフォントが改善されて日本語の太字も対応できるようになった。文字を重ねて擬似的に太く見せるなどの手段は必要なくなった。なので数日前に行われた Edge の自動アップデートで疑似的な手段は止めた。しかしながら、Xperia 10 V では太字フォントが無いのでダメ・・・ということではなかろうかと思っています。

違うかなぁ・・・ でも、数日前は font-weight: bold; で日本語も太字になっていたのは間違いないし、上記は当たらずとも遠からずだと思うのですが。

なお、自分の Xperia 10 V の Edge で見ても font-weight: bold; で日本語が太字になっているサイトがありましたが、たぶん、Web フォントで対応しているのだろうと思います。

Tags: , , , , , ,

その他

Task.Status が Canceled に変わるタイミング

by WebSurfer 21. August 2025 12:41

Microsoft のドキュメント「タスクの一覧を取り消す」のサンプルコードを試して気づいた話ですが、CancellationTokenSource.Cancel メソッドを実行した際、CancellationToken を受け取る非同期メソッドの Task.Status が Canceled に変わるタイミングが .NET Framework 4.8.1 と .NET 8.0 / 9.0 では異なるということがあったので、備忘録として書いておきます。

サンプルコードはこの記事の下の方に載せておきます。ターゲットフレームワークを .NET 8.0 または .NET 9.0 とした場合と、.NET Framework 4.8.1 とした場合のサンプルコードの動きの違いは、ダウンロードの途中で Enter キーを押してキャンセルをかけた時、後者では Main メソッドの中の try - catch ブロックが実行されないことです。

その原因は、タスク cancelTask の中の s_cts.Cancel(); が実行された時、CancellationToken を受け取る非同期メソッドの Task.Status が Canceled に変わるタイミングが異なるからです。

.NET Framework 4.8.1 の場合、Cancel メソッドの実行で即 Task.Status が WaitingForActivation から Canceled に変わります。

一方、.NET 8.0 / 9.0 の場合、Cancel メソッドの実行では Task.Status は WaitingForActivation のまま変わりません。Task を await して初めて Status が Canceled に変わります。

以下にサンプルコードを検証した際の詳細を書いておきます。

.NET Framework 4.8.1

下の画像は、ターゲットフレームワークを .NET Framework 4.8.1 としたプロジェクトを Visual Studio 2022 でデバッグ実行した時のものです。

.NET Framework 4.8.1 の場合

ダウンロード途中で(すなわち、SumPageSizesAsync メソッド内の foreach ループが回っているとき)Enter キーを押した結果、タスク cancelTask の中の s_cts.Cancel(); が実行され、ProcessUrlAsync メソッド内の非同期メソッドの実行がキャンセルされ、Task.WhenAny メソッドでその引数に設定されたタスク cancelTask と sumPageSizesTask の内 sumPageSizesTask が先に完了と判断され、await による待機を抜け、次の行の if 文に制御が移ったところです。

その結果、if 文の条件 finishedTask == cancelTask は false となるので try - catch ブロックは実行されません。その際に気を付けなければならないのは、await sumPageSizesTask; は実行されないので OperationCanceledException はスローされないということです。

なので、下に載せたサンプルコードのように、OperationCanceledException を catch して何らかの処置を行いたい場合、そのためのコードを追加する必要があります。

ただし、ダウンロードが完了してから(すなわち SumPageSizesAsync メソッド内の foreach ループを抜けて CancellationToken を引数に取る非同期メソッドがすべて完了してから)タスク cancelTask の中の s_cts.Cancel(); が実行された場合は様子が違ってきます。タスク cancelTask の方が先に完了したと判断され、try - catch ブロックは実行されます。(実際にそれを試すには、foreach ループの後に 1 行 await Task.Delay(3000); を追加して、そこで止まっているときに Enter キーを押してみてください)

.NET 8.0 / 9.0

ターゲットフレームワークを .NET 8.0 または 9.0 としたプロジェクトで上と同様にデバッグを行うと以下の画像のようになります。

.NET 8.0 の場合

ダウンロード途中で Enter キーを押すとタスク cancelTask 内の s_cts.Cancel(); が実行されるのですが、その時点では ProcessUrlAsync メソッド内の非同期メソッドの実行がキャンセルされないのか、タスク sumPageSizesTask の Status は WaitingForActivation になっています。一方、タスク cancelTask の Status は RanToCompletion となり、Task.WhenAny メソッドでその引数に設定されたタスク cancelTask が先に完了したと判断され、await による待機を抜け、次の行の if 文に制御が移っています。

この場合、if 分の条件 finishedTask == cancelTask は true となるので try - catch ブロックが実行されます。try 句の中の await sumPageSizesTask; により OperationCanceledException がスローされ、catch 句の中のコードが実行されます。

タスク sumPageSizesTask の Status が WaitingForActivation から Canceled に変わるのは try 句の中の await sumPageSizesTask; で await による待機が終わった時点になります。

以下の画像を見てください。上の画像の if 文でブレークポイントで止まったところからステップ実行して、await sumPageSizesTask; でスローされた OperationCanceledException を catch したところです。sumPageSizesTask の Status が Canceled に変わっています。

.NET 8.0 の場合

try 句の中の await sumPageSizesTask; の行で例外がスローされるので、その下の Console.WriteLine("Download task completed before cancel request was processed."); はスキップされます。このメッセージは「Enter キーが押されたがキャンセルが間に合わずダウンロードが完了してしまった」という意味なのですが、手動で Enter キーを押してこのメッセージを表示するのは無理があるので注意してください。

このメッセージが表示されることを試すには、SumPageSizesAsync メソッド内の foreach ループの後に 1 行 await Task.Delay(3000); を追加して、そこで止まっているときに Enter キーを押してみてください。その時点では CancellationToken を渡された非同期メソッドの実行は終わっているので、タスク cancelTask の中の s_cts.Cancel(); の実行では OperationCanceledException 例外はスローされません。結果、await sumPageSizesTask; の下の行の Console.WriteLine("Download task completed before cancel request was processed."); が実行されます。


.NET Framework 4.8.1 の場合の s_cts.Cancel(); の実行で CancellationToken を渡された非同期メソッドの実行が即キャンセルされるという動きの方が自分としては納得できるのですが、何らかの理由で .NET 8.0(もっと前からかも)で変更したのでしょうか?

そのあたりの説明がある Microsoft のドキュメントは見つけられませんでしたが、とにかく違いがあるということは覚えておいた方が良さそうと思って、備忘録として残しておくことにした次第です。


以下に、上に述べた検証に用いた Microsoft ドキュメントのサンプルコードを載せておきます。

using System.Diagnostics;

class Program
{
    static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

    static readonly HttpClient s_client = new HttpClient
    {
        MaxResponseContentBufferSize = 1_000_000
    };

    static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dynamics365",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://learn.microsoft.com/system-center",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            // "https://learn.microsoft.com/maui" 404 エラーになるのでコメントアウト
    };

    static async Task Main()
    {
        Console.WriteLine("Application started.");
        Console.WriteLine("Press the ENTER key to cancel...\n");

        Task cancelTask = Task.Run(() =>
        {
            while (Console.ReadKey().Key != ConsoleKey.Enter)
            {
                Console.WriteLine("Press the ENTER key to cancel...");
            }

            Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
            s_cts.Cancel();
        });

        Task sumPageSizesTask = SumPageSizesAsync();

        Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
        if (finishedTask == cancelTask)
        {
            // wait for the cancellation to take place:
            try
            {
                await sumPageSizesTask;
                Console.WriteLine("Download task completed before cancel request was processed.");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Download task has been cancelled.");
            }
        }

        Console.WriteLine("Application ending.");
    }

    static async Task SumPageSizesAsync()
    {
        var stopwatch = Stopwatch.StartNew();

        int total = 0;
        foreach (string url in s_urlList)
        {
            int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
            total += contentLength;
        }

        stopwatch.Stop();

        Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
        Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
    }

    static async Task<int> ProcessUrlAsync(string url, 
                                                 HttpClient client, 
                                                 CancellationToken token)
    {
        HttpResponseMessage response = await client.GetAsync(url, token);

        // .NET Framework 4.8.1 の ReadAsByteArrayAsync には
        // CancellationToken を引数に取るオーバーロードはない
        // ので下の引数 token は削除すること
        byte[] content = await response.Content.ReadAsByteArrayAsync(token);

        Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

        return content.Length;
    }
}

Tags: , , , ,

.NET Framework

About this blog

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

Calendar

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

View posts in large calendar