BlazorでWeb APIが404になる原因と解決策|相対URL・HttpClient BaseAddress・CORSの正しい設定

Blazor アプリから Web API を呼び出した瞬間に「404 (Not Found)」が返ってくる場合、原因の多くは “API が存在しない” のではなく「相対 URL が想定と違う宛先に解決されている」ことです。本記事では、404 の見分け方から、HttpClient の BaseAddress をどこにどう登録すべきか、別オリジン時に必須になる CORS までを、実務で再現しやすい形で整理します。

目次

Blazor から Web API を呼ぶと 404 になる典型パターン

Blazor 側でよくある呼び出しは、例えば次のような相対 URL です。

@page "/employees"
@inject HttpClient Http

<h3>Employees</h3>

@code {
    private Employee[]? employees;

    protected override async Task OnInitializedAsync()
    {
        // 相対URL(ここが 404 の本命になりやすい)
        employees = await Http.GetFromJsonAsync<Employee[]>("api/employees");
    }

    public record Employee(int Id, string Name);
}

このとき返るエラーが、次のようなものです。

  • Response status code does not indicate success: 404 (Not Found)

ここで重要なのは、相対 URL は「現在のオリジン(ホスト名+ポート+スキーム)」や「現在のパス」を基準に解決されるという点です。つまり、Blazor 画面が動いている URL と、実際の API が動いている URL がズレていると、意図しない場所へリクエストが飛び、結果として 404 になります。

まず押さえる:相対 URL はどこへ飛ぶのか

api/employees のような相対 URL は、「今表示している Blazor アプリの場所」を基準に解決されます。たとえば次のようにズレます。

Blazor 画面の URL(基準)呼び出しコード実際に飛ぶリクエスト URLAPI の本当の場所起きやすい結果
https://localhost:5001/api/employeeshttps://localhost:5001/api/employeeshttps://localhost:7009/api/employees404(別アプリに飛ぶ)
https://example.com/myapp/api/employeeshttps://example.com/myapp/api/employeeshttps://example.com/api/employees404(サブパス配信時にズレる)
https://example.com/myapp//api/employeeshttps://example.com/api/employeeshttps://example.com/api/employees成功しやすい(同一オリジンでルート固定)

ここから分かる通り、404 の本筋は「API が存在しない」ではなく「別の場所に飛んでいる」ことです。まずは “どこへ飛んでいるか” を 1 分で確定させるのが最短ルートになります。

404 の切り分け手順:まず「Request URL」を見る

404 を見たら、最初にやるべきことは次の 2 つです。

  1. ブラウザーの開発者ツール(Network タブ)で該当リクエストを開く
  2. Request URL と Status Code を確認する

そして、Request URL が “想定の API の URL” になっていなければ、それが原因です。逆に Request URL が正しくても 404 なら、API 側のルーティング(パス)が違う可能性が高いです。

チェック項目どこを見るよくある発見次の一手
Request URL が合っているかNetwork → Request URLポートが Blazor 側になっているBaseAddress/絶対 URL を見直す
スキーム(http/https)が合っているかRequest URLhttps のはずが http へAPI の起動 URL と揃える
パスが API のルートと一致しているかRequest URL と Swagger実際は /employees なのに /api/employees を呼んでいるAPI のルーティングを正に合わせる
API が動いているかAPI の Swagger へ直接アクセスそもそも API が起動していない/別ポート起動構成(複数プロジェクト起動など)を修正

なお、CORS が原因の場合は 404 とは違う症状(コンソールに CORS エラーが出る、プリフライトが失敗する等)になることが多いです。CORS は後半で扱いますが、404 だけが出ているときはまず URL 解決とルーティングのズレを疑うのが近道です。

解決策:状況別に最適な直し方を選ぶ

修正方法は大きく 3 つに分かれます。

  • 絶対 URL を使う(すぐ直る、ただし環境差分の管理が必要)
  • HttpClient の BaseAddress を正しく設定して、相対パスを API 側に向ける(運用向き)
  • 別オリジンなら CORS を許可する(将来の詰まりを防ぐ)

解決策1:絶対 URL を指定して “飛び先” を固定する

最も分かりやすい対処は、呼び出し側でホスト名・ポートまで含めた絶対 URL を指定することです。

var employees = await Http.GetFromJsonAsync<Employee[]>(
    "https://localhost:7009/api/employees"
);

これで 404 が消えるなら、原因はほぼ 100%「相対 URL が Blazor 側のオリジンへ解決されていた」です。

ただし、絶対 URL は次の点で運用が面倒になりがちです。

  • 開発・検証・本番で URL が変わる(環境ごとの差分が増える)
  • 将来的に API のドメインが変わると修正箇所が散らばる

そのため、長期運用前提なら「BaseAddress を設定して相対パス運用」に寄せるのがおすすめです。

解決策2:HttpClient の BaseAddress を正しく設定して相対 URL を活かす

相対 URL を保ったまま “飛び先” を API に向けたい場合、HttpClient の BaseAddress を設定します。イメージは次の通りです。

  • BaseAddress:https://localhost:7009/
  • 呼び出し:api/employees
  • 結果:https://localhost:7009/api/employees

ここで重要なのが、質問にもあった「AddHttpClient に BaseAddress を設定したのに効かない」の典型原因です。これは多くの場合、次のどれかに該当します。

  • “登録した場所” が違う(クライアントで動くのにサーバー側に登録している)
  • “使っている HttpClient” が違う(AddHttpClient で作ったクライアントを使っていない)
  • レンダリングモードが混在していて、実行場所によって DI が別になっている

これを避けるために、まずは「あなたの Blazor がどのモデルか」「HttpClient がどこで動くか」を整理します。

どの Blazor かで “BaseAddress を入れる場所” が変わる

構成コンポーネントの実行場所BaseAddress を設定すべき場所相対 URL の基準よくある落とし穴
Blazor WebAssembly(単体)ブラウザーClient の Program.csブラウザーのオリジンServer 側に AddHttpClient しても効かない
Blazor ServerサーバーServer の Program.csサーバーからの HTTP相対 URL が “Blazor サーバー自身” を向く
Blazor Web App(.NET 8+、Interactive Server)サーバーServer の Program.csサーバーからの HTTPブラウザー実行ではないので CORS と挙動が異なる
Blazor Web App(.NET 8+、Interactive WebAssembly / Auto)サーバー+ブラウザー(混在)Server と Client 両方実行場所により変わるServer に入れた BaseAddress が Client に届かない

ここがズレると、いくら BaseAddress を設定しても「効いていない」ように見えます。次の章から、構成別に “効く書き方” を具体例で示します。

Blazor WebAssembly の正しい BaseAddress 設定(クライアント側)

Blazor WebAssembly はブラウザー上で実行されるため、Client プロジェクトの Program.cs(または単体 WASM の Program.cs)で HttpClient を登録します。

同一オリジン(Blazor と API が同じホスト/ポート)で完結するなら、基本形は次です。

using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// 同一オリジン前提:ホストされている場所(Blazor アプリの URL)を基準にする
builder.Services.AddScoped(sp =>
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

しかし、質問のケースのように API が別ポート(例:https://localhost:7009/)なら、この基本形では Blazor 側(例:https://localhost:5001/)に飛んでしまいます。そこで、API 用の HttpClient を別に用意します。おすすめは “名前付き” または “型付き” クライアントです。

名前付き HttpClient(IHttpClientFactory)

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// 例:API が別オリジン(別ポート)にあるケース
builder.Services.AddHttpClient("Api", client =>
{
    client.BaseAddress = new Uri("https://localhost:7009/");
});

await builder.Build().RunAsync();

コンポーネント側は IHttpClientFactory から作成して使います。

@inject IHttpClientFactory HttpClientFactory

@code {
    private Employee[]? employees;

    protected override async Task OnInitializedAsync()
    {
        var http = HttpClientFactory.CreateClient("Api");
        employees = await http.GetFromJsonAsync<Employee[]>("api/employees");
    }

    public record Employee(int Id, string Name);
}

ここが重要: AddHttpClient を書いたのに @inject HttpClient Http のまま使っていると、“あなたが設定した BaseAddress を持つ HttpClient” ではない別の HttpClientが注入されてしまい、期待通りにならないことがあります。設定したクライアントを使うなら、IHttpClientFactory(名前付き)か、次の型付きクライアントに寄せるのが安全です。

型付き HttpClient(おすすめ:利用箇所が増えても崩れにくい)

public sealed class EmployeeApiClient
{
    private readonly HttpClient _http;

    public EmployeeApiClient(HttpClient http) => _http = http;

    public Task<Employee[]?> GetEmployeesAsync()
        => _http.GetFromJsonAsync<Employee[]>("api/employees");

    public record Employee(int Id, string Name);
}
// Program.cs(Client)
builder.Services.AddHttpClient<EmployeeApiClient>(client =>
{
    client.BaseAddress = new Uri("https://localhost:7009/");
});
@inject EmployeeApiClient Api

@code {
    private EmployeeApiClient.Employee[]? employees;

    protected override async Task OnInitializedAsync()
    {
        employees = await Api.GetEmployeesAsync();
    }
}

型付きにしておくと、呼び出し側が「常にこのクライアントを使う」形になるため、BaseAddress の “すり替わり事故” を防ぎやすくなります。

Blazor Server の正しい BaseAddress 設定(サーバー側)

Blazor Server はコンポーネントの処理がサーバーで動きます。したがって、Server の Program.cs(1 つのプロジェクトならその Program.cs)に AddHttpClient を書くのが基本です。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents();
builder.Services.AddHttpClient("Api", client =>
{
    client.BaseAddress = new Uri("https://localhost:7009/");
});

var app = builder.Build();
app.MapRazorComponents<App>();
app.Run();

Server 側のコンポーネントからは IHttpClientFactory で作成して使います。

@inject IHttpClientFactory HttpClientFactory

@code {
    private Employee[]? employees;

    protected override async Task OnInitializedAsync()
    {
        var http = HttpClientFactory.CreateClient("Api");
        employees = await http.GetFromJsonAsync<Employee[]>("api/employees");
    }

    public record Employee(int Id, string Name);
}

Blazor Server では “ブラウザーの CORS” は基本的に関係ありません(サーバーからサーバーへ HTTP リクエストするため)。一方で、相対 URL を使うと「Blazor サーバー自身」を叩くことになりやすい点が落とし穴です。API が別ホスト/別ポートなら、BaseAddress を明示しましょう。

Blazor Web App(.NET 8+)の落とし穴:実行場所が混在する

Blazor Web App(.NET 8 以降のテンプレート)では、レンダリングモードによってコンポーネントの実行場所が変わります。

  • Interactive Server:サーバーで実行
  • Interactive WebAssembly:ブラウザーで実行
  • Auto:最初はサーバー → 以降はブラウザー(混在)

このとき、Server 側の Program.cs に書いた AddHttpClient は、ブラウザーで動くコンポーネントには届きません。これが「BaseAddress を設定したのに効かない」最大の原因です。

対策はシンプルで、WebAssembly で動く可能性があるなら Client 側にも同じ設定を入れることです(Client プロジェクトがある構成の場合)。

Server 側(Program.cs)

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();

builder.Services.AddHttpClient("Api", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Api:BaseUrl"]!);
});

var app = builder.Build();

app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode();

app.Run();

Client 側(Client/Program.cs)

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddHttpClient("Api", client =>
{
client.BaseAddress = new Uri(builder.Configuration["Api:BaseUrl"]!);
});

await builder.Build().RunAsync();

このように両方へ登録しておくと、同じコンポーネントが Server 実行でも WASM 実行でも同じ書き方で API を叩けます。

もし「Client 側に設定を二重で持ちたくない」「CORS を避けたい」という場合は、設計として “ブラウザーから直接 API を叩かない” 方式(いわゆる BFF:Backend for Frontend)も有効です。例えば Blazor サーバー側で API を呼び、Blazor 側へ必要なデータだけ返す、という構成にすると、ブラウザーの CORS 問題を避けられます(ただし認証/負荷/キャッシュなど別の設計観点が増えます)。

相対パスの “先頭スラッシュ” が 404 を生むケース(サブディレクトリ配信)

オリジンが同じでも、アプリをサブディレクトリ(例:https://example.com/myapp/)で配信していると、api/employees が https://example.com/myapp/api/employees になり、API がルート(/api/employees)にある場合に 404 になります。

この場合は、次のように先頭にスラッシュを付けてルート固定にすると解決します(同一オリジン前提)。

employees = await Http.GetFromJsonAsync<Employee[]>("/api/employees");

BaseAddress を API のルート(https://example.com/)にしているつもりでも、実際は https://example.com/myapp/ が BaseAddress になっているケースは現場でよくあります。Network の Request URL を見れば一発で判定できます。

解決策3:別オリジンなら CORS を必ず設定する

Blazor と API が別オリジン(例:Blazor が https://localhost:5001、API が https://localhost:7009)なら、ブラウザーは同一生成元ポリシーにより API をブロックします。これを許可する仕組みが CORS です。

重要なのは、CORS は “Blazor 側” ではなく API 側に設定するということです。

ASP.NET Core Web API 側の CORS 設定例

開発中に最小で通したいなら「Blazor のオリジンだけ許可」するのが安全です。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowBlazor", policy =>
    {
        policy.WithOrigins("https://localhost:5001") // Blazor の URL
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseHttpsRedirection();

// 重要:MapControllers より前に適用(一般的な構成)
app.UseCors("AllowBlazor");

app.MapControllers();

app.Run();

認証で Cookie を使うなど、資格情報(Credentials)を伴う場合はさらに注意が必要です。

  • AllowCredentials() を付ける
  • AllowAnyOrigin() は併用できない(具体的な Origins を列挙する)
policy.WithOrigins("https://localhost:5001")
      .AllowAnyHeader()
      .AllowAnyMethod()
      .AllowCredentials();

なお、CORS が原因だとブラウザーのコンソールに “Access-Control-Allow-Origin が無い” などのエラーが出ることが多く、404 とは見え方が変わります。とはいえ、API が別オリジンなら将来的に必ず壁になるため、早めに整えておくのが得策です。

「404」と「CORS」と「接続失敗」を取り違えないための早見表

症状開発者ツールでの見え方主な原因優先して直すところ
404 Not FoundNetwork に 404 が出る(Response も返る)URL/ポート/パスが違う、ルーティングが違うRequest URL と API のルートを一致させる
CORS エラーConsole に CORS エラー。Network は (blocked) や preflight 失敗API が別オリジンで許可されていないAPI 側で CORS ポリシーを追加
ERR_CONNECTION_REFUSED / タイムアウトNetwork が赤字、応答が返らないAPI が起動していない、ポートが違う、FW/プロキシAPI の起動状態と URL を確認
401/403Network に 401/403、レスポンスが返る認証/認可、トークンや Cookie が不足認証方式と送信方法(Bearer/Cookie)を整理
500Network に 500、API のログに例外API の内部例外API 側のログ・例外を修正

“BaseAddress を入れたのに効かない” を最短で潰すチェックリスト

BaseAddress まわりは、設定が正しくても “使っているクライアントが違う” だけで崩れます。次を順番に確認してください。

  • あなたのコンポーネントはブラウザーで動いているか?
    • 動いている(WebAssembly/Auto の後半)→ Client 側 Program.cs に登録が必要
    • 動いていない(Server)→ Server 側 Program.cs に登録が必要
  • AddHttpClient で登録したクライアントを本当に使っているか?
    • 名前付きなら IHttpClientFactory.CreateClient("Api") を使う
    • 型付きなら対象クラスを DI で受け取って使う
    • @inject HttpClient Http だけだと “別の HttpClient” の可能性がある
  • Network の Request URL は BaseAddress どおりか?
    • 違うなら、登録場所 or 利用クライアントの取り違えが濃厚

この 3 点を満たした状態でまだ 404 なら、次に疑うべきは API 側のルーティング(パスが違う)です。Swagger(OpenAPI)が有効なら、表示されているパスをそのまま呼んで一致させましょう。

環境差分に強くする:API の URL を 1 箇所で管理する

ローカルでは https://localhost:7009/、本番では別ドメイン、というのは普通に起きます。BaseAddress をソースに直書きせず、設定ファイルに寄せるのが運用上ラクです。

appsettings.json の例(Server 側)

{
  "Api": {
    "BaseUrl": "https://localhost:7009/"
  }
}

Program.cs の例(Server 側)

builder.Services.AddHttpClient("Api", client =>
{
    var baseUrl = builder.Configuration["Api:BaseUrl"];
    client.BaseAddress = new Uri(baseUrl!);
});

Blazor WebAssembly / Client 側も同様に設定を読み込めるようにしておけば、環境ごとの切り替えが容易になります(Client 側は公開される前提の値だけを入れる、などの設計配慮は必要です)。

実務でおすすめの結論:404 は “URL 解決” と “登録場所” を最優先で潰す

Blazor の 404 は、次の順で見ればほぼ迷いません。

  1. Network で Request URL を確認し、想定の API(ホスト/ポート/パス)に飛んでいるかを確定する
  2. 飛んでいないなら、絶対 URL で一度通して原因を確定 → その後 BaseAddress 運用に整える
  3. BaseAddress を使うなら、Blazor の実行場所(Server/WASM/Auto)に合わせて Program.cs の登録場所を正しくする
  4. 別オリジンなら、API 側に CORS を設定してブラウザーでブロックされないようにする

この流れで直すと、「たまたま動いた」を避けて再現性のある状態にできます。特に .NET 8+ の Blazor Web App(Interactive WebAssembly / Auto)では “サーバーに設定したつもりがクライアントに効いていない” が頻発するため、DI の登録場所と利用クライアント(名前付き/型付き)をセットで揃えるのが安定策です。

この記事を書いた人

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

コメント

コメントする

目次