httpclient-factory について
外部 HTTP リクエストを扱う .NET 開発者向けの HttpClient Factory スキルです。クライアントのライフサイクル管理、レジリエンスハンドラー、横断的関心事を扱う委譲ハンドラーを説明します。
主な機能
- 01ファクトリー管理の HTTP クライアント
- 02キー付きクライアント登録
- 03標準レジリエンスハンドラー
- 04認証と相関 ID 用の委譲ハンドラー
- 05モックハンドラーによるテスト
活用シーン
- 01リトライ付きの外部 API 呼び出し
- 02リクエストへの認証トークン追加
- 03相関 ID の伝播
- 04HTTP 連携のテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill httpclient-factoryインストール後、Claude Code で/httpclient-factoryを入力して読み込まれたか確認します。
詳細
HttpClient ファクトリー
基本原則
- リクエストごとに
new HttpClient()を使わないでください —HttpClientを直接作成すると、高負荷時にソケットが枯渇し、DNS の変更も反映されません。ハンドラーの有効期間を管理するには、IHttpClientFactoryを使用してください。 - 型指定クライアントよりキー付きクライアントを使用してください — .NET 10 では、キー付き DI(
.AddAsKeyed())が推奨されるパターンです。シングルトンに保持された型指定クライアントは、ハンドラーのローテーションを気付かれないまま妨げます。 - 回復性は必須です — 外部への HTTP 呼び出しにはすべて、リトライ、サーキットブレーカー、タイムアウトが必要です。
AddStandardResilienceHandler()を使えば、適切な既定値を 1 行で設定できます。 - 横断的な関心事には DelegatingHandler を使用してください — 認証トークン、相関 ID、ログ記録は、サービスメソッドに散在させず、ハンドラーパイプラインに配置します。
パターン
回復性を備えた名前付きクライアント
builder.Services.AddHttpClient("github", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddStandardResilienceHandler();
// Usage via factory
public sealed class GitHubService(IHttpClientFactory factory)
{
public async Task<Repo?> GetRepoAsync(string owner, string name, CancellationToken ct)
{
var client = factory.CreateClient("github");
return await client.GetFromJsonAsync<Repo>($"repos/{owner}/{name}", ct);
}
}
キー付きクライアント(.NET 10 で推奨)
名前付きクライアントの設定の柔軟性と、直接のインジェクションを組み合わせます。文字列による検索は不要です。
builder.Services.AddHttpClient("payments", client =>
{
client.BaseAddress = new Uri("https://api.payments.example.com/");
})
.AddStandardResilienceHandler()
.AddAsKeyed(); // Register as keyed scoped service
// Inject directly — no IHttpClientFactory needed
app.MapPost("/charge", async (
[FromKeyedServices("payments")] HttpClient httpClient,
ChargeRequest request,
CancellationToken ct) =>
{
var response = await httpClient.PostAsJsonAsync("charges", request, ct);
return response.IsSuccessStatusCode
? TypedResults.Ok()
: TypedResults.Problem("Payment failed");
});
グローバルに有効化するには、builder.Services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); を使用します。
標準の回復性ハンドラー
AddStandardResilienceHandler() は 5 つの戦略を連結します。
| 戦略 | 既定値 |
|---|---|
| レートリミッター | 同時リクエスト数 1000 |
| 合計タイムアウト | 30 秒 |
| リトライ | 3 回のリトライ、ジッターを伴う指数バックオフ |
| サーキットブレーカー | 失敗率 10% で開く |
| 試行ごとのタイムアウト | 1 回の試行につき 10 秒 |
builder.Services.AddHttpClient("api")
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 5;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15);
// Disable retries for non-idempotent methods
options.Retry.DisableForUnsafeHttpMethods();
});
認証トークン注入用 DelegatingHandler
public sealed class AuthenticationHandler(ITokenService tokenService)
: DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await tokenService.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
// Registration
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("api")
.AddHttpMessageHandler<AuthenticationHandler>()
.AddStandardResilienceHandler();
相関 ID 伝播用 DelegatingHandler
public sealed class CorrelationIdHandler(IHttpContextAccessor httpContextAccessor)
: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (httpContextAccessor.HttpContext?.Request.Headers
.TryGetValue("X-Correlation-Id", out var correlationId) is true)
{
request.Headers.Add("X-Correlation-Id", correlationId.ToString());
}
return base.SendAsync(request, cancellationToken);
}
}
SocketsHttpHandler の設定
builder.Services.AddHttpClient("advanced")
.UseSocketsHttpHandler((handler, _) =>
{
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2);
handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1);
handler.MaxConnectionsPerServer = 100;
handler.AutomaticDecompression =
DecompressionMethods.GZip | DecompressionMethods.Brotli;
});
モックハンドラーを使用したテスト
public sealed class MockHttpHandler(
HttpStatusCode statusCode,
string content) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(statusCode)
{
Content = new StringContent(content, Encoding.UTF8, "application/json")
});
}
}
// In test
var handler = new MockHttpHandler(HttpStatusCode.OK, """{"id":1}""");
var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.test/") };
var service = new MyService(client);
アンチパターン
リクエストごとに HttpClient を作成しない
// BAD — socket exhaustion under load, ignores DNS changes
public async Task<string> GetDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com/data");
}
// GOOD — factory-managed
public async Task<string> GetDataAsync(CancellationToken ct)
{
var client = factory.CreateClient("api");
return await client.GetStringAsync("https://api.example.com/data", ct);
}
シングルトンに型指定クライアントを保持しない
// BAD — transient HttpClient captured by singleton defeats handler rotation
services.AddSingleton<MySingletonService>();
services.AddHttpClient<MySingletonService>();
// GOOD — use keyed client or IHttpClientFactory in singletons
services.AddSingleton<MySingletonService>();
services.AddHttpClient("myservice").AddAsKeyed(ServiceLifetime.Singleton);
共有クライアントの DefaultRequestHeaders を変更しない
// BAD — not thread-safe
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// GOOD — use DelegatingHandler or per-request HttpRequestMessage
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/data");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
await httpClient.SendAsync(request, ct);
CancellationToken を忘れない
// BAD — no cancellation support
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1");
// GOOD — always pass CancellationToken
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1", cancellationToken);
複数の回復性ハンドラーを重ねて設定しない
// BAD — conflicting resilience strategies
builder.AddStandardResilienceHandler();
builder.AddStandardHedgingHandler();
// GOOD — one standard handler, or a custom pipeline
builder.AddStandardResilienceHandler();
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| 新しい .NET 10 プロジェクト | AddAsKeyed() を使ったキー付きクライアント |
| シングルトンサービスで HttpClient が必要 | IHttpClientFactory を介した名前付きクライアント、またはキー付きシングルトン |
| 外部 API の呼び出し | すべてのクライアントに AddStandardResilienceHandler() を設定 |
| 認証トークンの注入 | AddHttpMessageHandler で登録した DelegatingHandler |
| ヘッジング(並列リクエスト) | レイテンシーが重要な呼び出しには AddStandardHedgingHandler() |
| 冪等でないメソッド | リトライオプションで DisableForUnsafeHttpMethods() を設定 |
| カスタムリトライロジック | AddResilienceHandler("name", builder => ...) |
| コネクションプールの制御 | PooledConnectionLifetime を指定した UseSocketsHttpHandler |
| API クライアントの生成 | AddRefitClient<T>() を使った Refit |
| 統合テスト | カスタムの HttpMessageHandler または MockHttpMessageHandler |
コメント
Markdown 対応