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(基準) | 呼び出しコード | 実際に飛ぶリクエスト URL | API の本当の場所 | 起きやすい結果 |
|---|---|---|---|---|
| https://localhost:5001/ | api/employees | https://localhost:5001/api/employees | https://localhost:7009/api/employees | 404(別アプリに飛ぶ) |
| https://example.com/myapp/ | api/employees | https://example.com/myapp/api/employees | https://example.com/api/employees | 404(サブパス配信時にズレる) |
| https://example.com/myapp/ | /api/employees | https://example.com/api/employees | https://example.com/api/employees | 成功しやすい(同一オリジンでルート固定) |
ここから分かる通り、404 の本筋は「API が存在しない」ではなく「別の場所に飛んでいる」ことです。まずは “どこへ飛んでいるか” を 1 分で確定させるのが最短ルートになります。
404 の切り分け手順:まず「Request URL」を見る
404 を見たら、最初にやるべきことは次の 2 つです。
- ブラウザーの開発者ツール(Network タブ)で該当リクエストを開く
- Request URL と Status Code を確認する
そして、Request URL が “想定の API の URL” になっていなければ、それが原因です。逆に Request URL が正しくても 404 なら、API 側のルーティング(パス)が違う可能性が高いです。
| チェック項目 | どこを見る | よくある発見 | 次の一手 |
|---|---|---|---|
| Request URL が合っているか | Network → Request URL | ポートが Blazor 側になっている | BaseAddress/絶対 URL を見直す |
| スキーム(http/https)が合っているか | Request URL | https のはずが 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 Found | Network に 404 が出る(Response も返る) | URL/ポート/パスが違う、ルーティングが違う | Request URL と API のルートを一致させる |
| CORS エラー | Console に CORS エラー。Network は (blocked) や preflight 失敗 | API が別オリジンで許可されていない | API 側で CORS ポリシーを追加 |
| ERR_CONNECTION_REFUSED / タイムアウト | Network が赤字、応答が返らない | API が起動していない、ポートが違う、FW/プロキシ | API の起動状態と URL を確認 |
| 401/403 | Network に 401/403、レスポンスが返る | 認証/認可、トークンや Cookie が不足 | 認証方式と送信方法(Bearer/Cookie)を整理 |
| 500 | Network に 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 は、次の順で見ればほぼ迷いません。
- Network で Request URL を確認し、想定の API(ホスト/ポート/パス)に飛んでいるかを確定する
- 飛んでいないなら、絶対 URL で一度通して原因を確定 → その後 BaseAddress 運用に整える
- BaseAddress を使うなら、Blazor の実行場所(Server/WASM/Auto)に合わせて Program.cs の登録場所を正しくする
- 別オリジンなら、API 側に CORS を設定してブラウザーでブロックされないようにする
この流れで直すと、「たまたま動いた」を避けて再現性のある状態にできます。特に .NET 8+ の Blazor Web App(Interactive WebAssembly / Auto)では “サーバーに設定したつもりがクライアントに効いていない” が頻発するため、DI の登録場所と利用クライアント(名前付き/型付き)をセットで揃えるのが安定策です。

コメント