IISに発行した.NET 9 Web APIが404・500になる原因とScalarドキュメントを表示する設定手順

.NET 9(ASP.NET Core 9)の Web API を IIS に発行したら、ルートが 404 になったり、開発中は表示されていた Scalar の API ドキュメントが本番 IIS では表示されない、さらに DB 接続を行う一部エンドポイントだけ 500 になる……という構成は、いくつかの「典型的な落とし穴」が重なって起きることが多いです。本記事では、その原因を丁寧に分解しながら、実際に IIS 上で安定して動く .NET 9 Web API+Scalar ドキュメント構成を作る手順をまとめます。

目次

IIS に発行した .NET 9 Web API で起こりがちな症状

先に、よくある症状と原因の対応関係を俯瞰しておきます。

症状典型的な原因対処の方向性
ルート / へアクセスすると 404Web API がルート用のエンドポイントを定義していない/ に別エンドポイント(例:Scalar)をリダイレクトする
/weatherforecast は動くが /api/player は 404コントローラのルーティング属性と実際の URL が一致していない[Route] / [HttpGet] などを見直し、URL を確認
開発環境では Scalar UI が出るのに IIS では出ないif (app.Environment.IsDevelopment()) 内でしか OpenAPI / Scalar を有効化していない本番環境でも MapOpenApiMapScalarApiReference を呼び出す
DB を触るエンドポイントだけ IIS で 500接続文字列の差・アプリプール ID の権限不足・ネットワーク制限などログで例外を確認し、接続先・認証・権限を修正
Scalar の URL が /scalar のときもあれば、/scalar/v1 のときもあるASP.NET Core と Scalar の既定設定の違い・バージョン差実際のルート(/scalar/scalar/v1)を確認し、必要ならリダイレクトで統一

.NET 9 Web API テンプレートと Scalar の関係を理解する

ASP.NET 9 では、従来テンプレートに含まれていた Swashbuckle(Swagger)の依存関係が外され、新しく Microsoft.AspNetCore.OpenApi パッケージによる OpenAPI 生成が既定になりました。テンプレートの Program.cs は概ね次のような構成になります。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.Run();

ここで生成されるのはあくまで「OpenAPI の JSON 仕様」であって、人間がブラウザで触りやすい UI(Swagger UI や Scalar UI)は自動では付きません。そのため、Scalar を使う場合は次の 2 つを自分で用意する必要があります。

  • OpenAPI の JSON を配信するエンドポイント(MapOpenApi
  • その JSON を読み込んで UI をレンダリングする Scalar(MapScalarApiReference

Scalar.AspNetCore パッケージを追加し、MapScalarApiReference を呼び出すと、既定では /scalar(多くの構成では /scalar/v1)に UI が割り当てられます。

ルート / が 404 になる理由と、Scalar にリダイレクトする実装

Web API テンプレートはルートに UI を持たない

ASP.NET Core Web API テンプレートは、もともと「SPA や Razor Pages のように HTML を返す UI」を持っていません。起動直後に HTML が見えるのは主に Swagger UI を使っている場合であり、.NET 9 の新テンプレートではこの UI 部分が削られています。

つまり、ルート / に対して何もエンドポイントを定義していなければ、IIS から「参照(Browse)」したときに 404 になるのは自然な挙動です。IIS の「既定ドキュメント」設定や「ディレクトリ ブラウズ」は、API アプリケーションには基本的に関係ありません。

ルート / を Scalar にリダイレクトする

「IIS の[参照]を押したときに API ドキュメント UI をすぐ開きたい」というケースでは、ルートを Scalar にリダイレクトするのがもっとも簡単です。

Program.cs を次のような構成にしてみます(.NET 9 を想定した最小構成の例)。

using Microsoft.AspNetCore.OpenApi;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// OpenAPI 仕様を生成
builder.Services.AddOpenApi();

// コントローラ(従来型)を使う場合
builder.Services.AddControllers();

var app = builder.Build();

// ★ 開発・本番を問わず OpenAPI / Scalar を有効化
app.MapOpenApi(); // 例: /openapi/v1.json

app.MapScalarApiReference(options =>
{
    options.Title = "My API";
    // 必要に応じてテーマやレイアウトの設定もここで行う
});

// ★ ルート / へ来たら Scalar にリダイレクト
app.MapGet("/", () => Results.Redirect("/scalar/v1"));

// API 本体
app.MapControllers();

app.Run();

これで、IIS マネージャーから「参照(Browse)」をクリックすると、ルート / にアクセス → 自動的に /scalar/v1 にリダイレクト → Scalar UI が表示、という流れになります。

もし環境によって /scalar がルートになる場合は、リダイレクト先を "/scalar" に変えるか、どちらか片方へ統一するための追加リダイレクト(/scalar -> /scalar/v1 など)を用意しておくと運用しやすくなります。

/api/player だけ 404 になる ― ルーティング属性の確認ポイント

コントローラの [Route] / [HttpXXX] 属性を読み解く

ASP.NET Core では、コントローラに付ける [Route] やアクションに付ける [HttpGet][HttpPost] などの属性で「どの URL に何の HTTP メソッドでアクセスできるか」が決まります。

たとえば、次のようなコントローラを考えます。

[Route("api/[controller]")]
[ApiController]
public class PlayerController : ControllerBase
{
    // すべてのプレイヤーを取得
    [HttpGet("GetAllPlayers")]
    public IActionResult GetAllPlayers()
    {
        return Ok(new[] { "Player1", "Player2" });
    }

    // 単一プレイヤーを取得
    [HttpGet("{id:int}")]
    public IActionResult GetPlayer(int id)
    {
        // ...
        return Ok();
    }
}

この場合、実際に有効なエンドポイントは次のようになります。

HTTP メソッドURL パス対応するアクション
GET/api/Player/GetAllPlayers(※大小文字は区別されない)GetAllPlayers
GET/api/Player/{id}(例:/api/Player/10GetPlayer

この構成では、GET /api/PlayerGET /api/player のように末尾にアクション名/ID が付いていない URL には、そもそもアクションがマップされていないため、404 になるのが正しい挙動です。

もし「GET /api/player で一覧を返したい」のであれば、次のように [HttpGet](パス指定なし)を追加します。

[Route("api/[controller]")]
[ApiController]
public class PlayerController : ControllerBase
{
    // GET /api/Player で一覧を返す
    [HttpGet]
    public IActionResult GetAllPlayers()
    {
        return Ok(new[] { "Player1", "Player2" });
    }

    // GET /api/Player/10 のように ID 指定で取得
    [HttpGet("{id:int}")]
    public IActionResult GetPlayer(int id)
    {
        // ...
        return Ok();
    }
}

ポイントは、「自分が叩いている URL と、属性で定義したルートが本当に一致しているか」を必ず確認することです。IIS 側の設定でこの挙動が変わることはほぼありません。

開発環境では Scalar が見えるのに IIS では見えない理由

よくあるパターン:if (IsDevelopment) の中に書いてしまう

Scalar や OpenAPI を公式ドキュメントやブログを参考に導入すると、次のように if (app.Environment.IsDevelopment()) ブロックの中にまとめて書いてしまうパターンがよくあります。

builder.Services.AddOpenApi();
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

開発中に Visual Studio から実行した場合、既定で ASPNETCORE_ENVIRONMENT=Development が設定されているため、このブロックが実行され、/openapi/v1.json/scalar/v1 が正常に表示されます。

一方、IIS に発行したアプリケーションは通常 Production 環境 として動作するため、上記のブロックは実行されず、結果として OpenAPI / Scalar の両方が無効になり、/scalar/scalar/v1 へアクセスしても 404/真っ白になる、という状況が発生します。

本番でも Scalar を表示したい場合の書き方

API ドキュメントを本番環境でも公開したいのであれば、OpenAPI および Scalar の設定を環境判定の外に出してしまうのがシンプルです。

builder.Services.AddOpenApi();
var app = builder.Build();

// ★本番でも常に OpenAPI / Scalar を有効化
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
    options.Title = "My API";
    // テーマやレイアウトの指定などもここに書ける
});

// 必要に応じて、/ -> /scalar/v1 のリダイレクト
app.MapGet("/", () => Results.Redirect("/scalar/v1"));

app.MapControllers();
app.Run();

逆に、「本番では API ドキュメントを公開したくない」場合は、Production 用の別スロットや管理用のみがアクセスできるサーバーにだけ上記設定を残す、といった運用で制御します。

Scalar ではなく OpenAPI JSON だけ動いている場合

トラブルシュートの際は、まず /openapi/v1.json にアクセスしたときに正しく JSON が返っているか を確認するのが有効です。

  • /openapi/v1.json が 404 → MapOpenApi() が呼ばれていない
  • /openapi/v1.json は OK だが Scalar が表示されない → MapScalarApiReference() 側のルート設定や、クライアント側のエラーの可能性

ブラウザの開発者ツール(F12)のネットワークタブで、Scalar UI が読み込もうとしている .json の URL と、その HTTP ステータスを確認しておくと、原因切り分けが早くなります。

IIS 上だけ 500 になる場合の原因と対処

「ローカルの dotnet run や Visual Studio では動くのに、IIS に発行した途端、特定のエンドポイント(主に DB 接続を伴うもの)だけ 500 になる」というパターンも非常によくあります。

この場合、フレームワークや IIS のバグであることはまれで、多くは 環境差による DB 接続/権限/ネットワークの問題 に起因しています。

まずは例外をログに残す

原因を特定するには、まず「どんな例外が実際に投げられているか」を知る必要があります。IIS 上の ASP.NET Core アプリでは、次の 2 つのログ手段を抑えておくとよいです。

  • アプリ内でのログ(例:ILogger や Serilog 等でファイル/イベントログへ出力)
  • web.config で有効化する stdout ログ(ASP.NET Core Module 経由)

web.config<aspNetCore> 要素を一時的に次のように変更します。

&lt;aspNetCore processPath="dotnet"
           arguments=".&lt;あなたのdll名&gt;.dll"
           stdoutLogEnabled="true"
           stdoutLogFile=".\logs\stdout"
           hostingModel="OutOfProcess"&gt;
  &lt;environmentVariables&gt;
    &lt;environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" /&gt;
  &lt;/environmentVariables&gt;
&lt;/aspNetCore&gt;

この設定を有効にしてから再現させると、アプリケーションルート配下の logs\stdout_*.log に例外のスタックトレースが出力されます。原因を特定したら、ログが肥大化しないよう stdoutLogEnabled="false" に戻すことをおすすめします。

DB 接続で起こりやすい環境差

IIS だけ 500 になるケースでは、以下のような「接続文字列/権限/ネットワーク」の差がよく問題になります。

要素開発環境(ローカル)IIS(本番環境)で起こる問題
接続先サーバー(localdb)\MSSQLLocalDB などローカルの SQLLocalDB はサービスとして常駐せず、IIS のアカウントからは基本使えない
認証方式Integrated Security=True で自分の Windows ユーザーを利用IIS のアプリプール ID に DB ログイン権限がないため、ログイン失敗
ネットワーク同一マシン内の SQL へ接続SQL が別サーバーにあり、ファイアウォールや TCP 設定で拒否されている
暗号化暗号化オプション無しでも接続できるサーバー側で暗号化が必須になっており、Encrypt=True 等を要求される

本番環境向けには、次のような形で接続文字列を見直すとよいでしょう。

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=SQLSERVER01;Database=MyAppDb;User Id=myapp_user;Password=StrongPassword!;Encrypt=True;TrustServerCertificate=True;"
  }
}
  • Windows 認証を使う場合は、SQL Server 側に IIS のアプリケーションプール ID(または専用サービスアカウント)をログインとして追加し、データベースへの権限(db_datareader / db_datawriter など)を付与します。
  • SQL 認証を使う場合は、専用の SQL ログインを作成し、接続文字列にユーザー名/パスワードを設定します。

環境変数・設定ファイルの食い違い

ASP.NET Core では、ASPNETCORE_ENVIRONMENT の値に応じて appsettings.Development.jsonappsettings.Production.json などの設定が読み込まれます。そのため、

  • ローカルでは appsettings.Development.json
  • 本番 IIS では appsettings.Production.json

のように、まったく別の接続文字列を見にいっていることもあります。例外ログに出ている実際の接続先(サーバー名や DB 名)を確認し、意図した設定ファイルが読み込まれているかを必ずチェックしましょう。

IIS 側の基本設定チェックリスト

ここまでの内容に加え、IIS 固有の設定として確認しておきたいポイントを整理します。

項目推奨・確認内容
アプリケーションプール.NET CLR バージョン:No Managed Code、マネージドパイプラインモード:統合
64bit必要に応じて「32 ビット アプリケーションの有効化」を false にして 64bit で動かす
web.configフォルダー発行時に生成された web.config が配置されているか確認(<aspNetCore> 要素を含む)
.NET Hosting Bundle対象バージョンの ASP.NET Core Hosting Bundle(.NET 9 相当を含む最新バンドル)がサーバーにインストールされているか
ディレクトリ ブラウズAPI アプリでは不要。オン・オフどちらでもよいが、トラブルシュートの観点では基本オフを推奨
既定のドキュメントWeb API では通常使わない。/ へのレスポンスはアプリ側(MapGet("/") 等)で制御する

最小構成サンプル:.NET 9 Web API + Scalar + IIS

ここまでの内容を踏まえて、「IIS 上で動作する最小構成」をサンプルとしてまとめます。

Program.cs の例

using Microsoft.AspNetCore.OpenApi;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// OpenAPI
builder.Services.AddOpenApi();

// 従来型コントローラを使う場合
builder.Services.AddControllers();

var app = builder.Build();

// ★OpenAPI と Scalar を常に有効化
app.MapOpenApi();            // /openapi/v1.json
app.MapScalarApiReference(); // /scalar または /scalar/v1

// ルートへのアクセスは Scalar にリダイレクト
app.MapGet("/", () =&gt; Results.Redirect("/scalar/v1"));

// API 本体
app.MapControllers();

app.Run();

PlayerController.cs の例

using Microsoft.AspNetCore.Mvc;

[Route("api/[controller]")]
[ApiController]
public class PlayerController : ControllerBase
{
    // GET /api/Player
    [HttpGet]
    public IActionResult GetAllPlayers()
    {
        return Ok(new[] { "Player1", "Player2" });
    }

    // GET /api/Player/10
    [HttpGet("{id:int}")]
    public IActionResult Get(int id)
    {
        return Ok(new { Id = id, Name = $"Player{id}" });
    }
}

appsettings.json(接続文字列例)

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=SQLSERVER01;Database=MyAppDb;User Id=myapp_user;Password=StrongPassword!;Encrypt=True;TrustServerCertificate=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

web.config(抜粋)

フォルダー発行すると自動生成される web.config をベースに、必要であれば stdout ログを一時的に有効にします。

&lt;configuration&gt;
  &lt;system.webServer&gt;
    &lt;handlers&gt;
      &lt;add name="aspNetCore"
           path="*"
           verb="*"
           modules="AspNetCoreModuleV2"
           resourceType="Unspecified" /&gt;
    &lt;/handlers&gt;

    &lt;aspNetCore processPath="dotnet"
               arguments=".\MyApi.dll"
               stdoutLogEnabled="false"
               stdoutLogFile=".\logs\stdout"
               hostingModel="OutOfProcess" /&gt;
  &lt;/system.webServer&gt;
&lt;/configuration&gt;

よくある勘違いとベストプラクティス

「404 は IIS 側の設定で直す」という思い込み

API の 404 は、ほぼ確実に「アプリ側でそのルートを定義していない」ことが原因です。IIS の「既定ドキュメント」やハンドラー設定をいじるのではなく、

  • MapGet("/")MapControllers() でルートを定義する
  • コントローラの [Route][HttpGet] を見直す

といった「アプリケーションルーティング」の観点から解決するのが正解です。

「開発環境と同じコードだから本番も動くはず」という誤解

開発環境ではローカルユーザーで実行され、本番では IIS のアプリプール ID で実行されます。さらに、読み込まれる設定ファイルや環境変数も変わります。特に DB 周りは、

  • 接続文字列(サーバー名/インスタンス名/認証方式)
  • アカウントに付与されている権限
  • ネットワークの到達性(ポート開放・TCP 設定)

といった前提が大きく異なるため、「コードは同じでも動作環境が違う」ことを前提に、ログと設定を丹念に確認していくことが大切です。

「Scalar のパスが環境ごとに違う」のを放置しない

Scalar の UI パスが /scalar なのか /scalar/v1 なのか、環境ごとにバラバラだと、「IIS では /scalar/v1 でないと開かない」といった不具合に見えがちです。環境差を減らすために、

  • MapScalarApiReference に明示的なルートパターンを指定する
  • ルート / から特定の URL(例:/scalar/v1)へリダイレクトしてしまう

といった対応で「どの環境でも同じ URL でアクセスできる」状態に揃えておくと、運用・トラブルシュートが非常に楽になります。

まとめ:404・500・Scalar 表示問題の「筋の良い」直し方

IIS に発行した .NET 9 Web API で 404 や 500 が発生し、Scalar の API ドキュメントも出ない場合、次の 3 点を押さえておくと、短時間で問題を収束させやすくなります。

  1. 404 はルーティングから疑う ルート / にエンドポイントが無ければ 404 になるのは正常です。必要であれば MapGet("/") で Scalar にリダイレクトしましょう。コントローラの URL も、[Route][HttpGet] などの属性と照合して「本当にその URL が正しいか?」を確認します。
  2. Scalar / OpenAPI は環境判定に依存させない 開発中だけ有効にするつもりが、本番でも必要だったというケースが多発します。本番でも API ドキュメントを公開するのであれば、MapOpenApiMapScalarApiReference を環境判定の外に出し、URL も /scalar/v1 などに統一しておくとよいでしょう。
  3. IIS だけ 500 のときは DB 接続・権限・環境変数を疑う ログ(stdout ログ+アプリ内ログ)を確認し、実際に投げられている例外を見ます。接続文字列・認証方式・アプリプール ID の権限・ネットワーク設定を見直すことで、多くの「IIS だけ 500」の問題は解消できます。

最終的に、「ルートは Scalar にリダイレクト」「全エンドポイントは正しい URL で 200 を返す」「本番の DB に正しく接続できる」という状態まで持っていければ、.NET 9 Web API + IIS + Scalar の構成はかなり安定して運用できるようになります。本記事をベースに、ぜひ自分の環境でもルーティングと環境設定を整理してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次