C# 今更ですが、HttpClientを使う

More than 1 year has passed since last update.


はじめに

RESTfulサービスが流行っているせいか、アプリケーションからHTTPのリクエストを投げたいことが多くなりました。HTTPリクエストと言えばHttpClientですが、使い方をすぐ忘れてしまうんですよね。まとまって書かれたサイトもない気がするので、まとめを兼ねて載せておきます。


リクエスト


とにかく、リクエストを送る

HTTPメソッドに対応したメソッドがあるので、それを呼べば良いです。

using (var client = new HttpClient())

{
var result1 = await client.GetAsync(@"http://hoge.example.com"); // GET
...

var result2 = await client.PostAsync(@"http://fuga.example.com"); // POST
...
}

HTTPメソッドは、TRACEやOPTIONSもあるのに、なぜかPATCHがありません。(WebDAV系のメソッドもありません)


HttpRequestMessaageを使い、SendAsync()を呼ぶ方法もあります。

var request = new HttpRequestMessage(HttpMethod.Get, @"http://hoge.example.com");

using (var client = new HttpClient())
{
var result = await client.SendAsync(request);
...
}


クエリパラメータを送る

URLエンコードは FormUrlEncodedContentを使ってしまうのが早いです。

var parameters = new Dictionary<string, string>()

{
{ "foo", "hoge" },
{ "bar", "fuga1 fuga2" },
{ "baz", "あいうえお" },
};
using (var client = new HttpClient())
{
var response =
await client.GetAsync($"http://foo.example.com?{await new FormUrlEncodedContent(parameters).ReadAsStringAsync()}");
...
}

実際に送られるリクエスト:(UTF-8でエンコードされる)

GET /?foo=hoge&bar=fuga1+fuga2&baz=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A HTTP/1.1

Host: foo.example.com
Connection: Keep-Alive


ボディを送る(Content-Typeを指定する)

POSTやPUTで普通にリクエストパラメータを送る場合は、FormUrlEncodedContentを使えば良い… のですが、スペースが + にエンコードされてしまいます。(クエリストリングは + で、それ以外は %20 でエンコードされるのが正しいらしいですが、大抵のWebサーバーはどちらでも解釈してくれると思います)

var parameters = new Dictionary<string, string>()

{
{ "foo", "hoge" },
{ "bar", "fuga1 fuga2" },
{ "baz", "あいうえお" },
};
var content = new FormUrlEncodedContent(parameters);

using (var client = new HttpClient())
{
var response = await client.PostAsync($"http://foo.example.com", content);
...
}

実際に送られるリクエスト:(Content-Typeが自動的に設定される)

POST / HTTP/1.1

Content-Type: application/x-www-form-urlencoded
Host: foo.example.com
Content-Length: 74
Expect: 100-continue
Connection: Keep-Alive

foo=hoge&bar=fuga1+fuga2&baz=%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A


文字列をそのまま送りたい場合は、StringContentを使えば良いですが、Content-Typeは text/plain になります。

var json = @"{""foo"":""hoge"", ""bar"":123, ""baz"":[""あ"", ""い"", ""う""]}";

var content = new StringContent(json, Encoding.UTF8);

using (var client = new HttpClient())
{
var response = await client.PostAsync($"http://foo.example.com", content);
...
}

実際のリクエスト:

POST / HTTP/1.1

Content-Type: text/plain; charset=utf-8
Host: foo.example.com
Content-Length: 54
Connection: Keep-Alive

{"foo":"hoge", "bar":123, "baz":["\343\201\202", "\343\201\204", "\343\201\206"]}


Content-Typeを application/jsonapplication/xml にしたい場合は、StringContentコンストラクターの第3引数で指定します。

var json = @"{""foo"":""hoge"", ""bar"":123, ""baz"":[""あ"", ""い"", ""う""]}";

var content = new StringContent(json, Encoding.UTF8, @"application/json");

using (var client = new HttpClient())
{
var response = await client.PostAsync($"http://foo.example.com", content);
...
}

実際のリクエスト:

POST / HTTP/1.1

Content-Type: application/json; charset=utf-8
Host: foo.example.com
Content-Length: 54
Connection: Keep-Alive

{"foo":"hoge", "bar":123, "baz":["\343\201\202", "\343\201\204", "\343\201\206"]}


文字列ではなく、バイナリーデータ(byte配列)を送りたい場合は、ByteArrayContent を使います。Content-Typeは次のように設定します。

var text = @"あいうえお";

var content = new ByteArrayContent(Encoding.UTF8.GetBytes(text));
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(@"text/hoge");

using (var client = new HttpClient())
{
var response = await client.PostAsync($"http://foo.example.com", content);
...
}

実際のリクエスト:

POST / HTTP/1.1

Content-Type: text/hoge
Host: foo.example.com
Content-Length: 15
Connection: Keep-Alive

...............


スペースが + でエンコードされるのがどうしても気に喰わない人は、自分でエンコードしてStringContentを使えば良いです。

var parameters = new Dictionary<string, string>()

{
{ "foo", "hoge" },
{ "bar", "fuga1 fuga2" },
{ "baz", "あいうえお" },
};
var body = string.Join(@"&", parameters.Select(pair => $"{pair.Key}={pair.Valueを自分でエンコード}"));
var content = new StringContent(body, Encoding.UTF8, @"application/x-www-form-urlencoded");

using (var client = new HttpClient())
{
var response = await client.PostAsync($"http://foo.example.com", content);
...
}


不正なContent-Typeを送る

StringContentの第3引数やヘッダーに設定(すぐ後で説明します)すると、System.FormatExceptionが発生してしまいます。ByteArrayContentを使いつつ、HttpContent.Headers.TryAddWithoutValidation()で無理矢理設定します。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Post, @"http://foo.example.com");
request.Content = new ByteArrayContent(Encoding.UTF8.GetBytes("{}"));
request.Content.Headers.TryAddWithoutValidation(@"Content-Type", @"hogehoge"); // OK
// request.Content.Headers.Add(@"Content-Type", @"hogehoge"); // NG
// request.Headers.TryAddWithoutValidation(@"Content-Type", "hogehoge"); // ヘッダーに付かない
var response = await client.SendAsync(request);
...
}

実際のリクエスト:

POST / HTTP/1.1

Content-Type: hogehoge
Host: foo.example.com
Content-Length: 2
Connection: Keep-Alive

{}


任意のヘッダーを送る

ヘッダーを送りたい場合は、HttpRequestMessage.Headers.Add()で設定すれば良いです。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Get, @"http://foo.example.com");
request.Headers.Add(@"X-Hoge", @"foo");

var response = await client.SendAsync(request);
...
}

実際のリクエスト:

GET / HTTP/1.1

X-Hoge: foo
Host: foo.example.com
Connection: Keep-Alive


不正なヘッダーを送りたい場合は、HttpRequestMessage.Headers.Add() を使うと System.FormatException が出るので、TryAddWithoutValidation()を使います。ただし、これを使っても付けられないヘッダーがあるので、実際に設定できたかどうかは戻り値の bool を確認したほうが良いでしょう。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Get, @"http://foo.example.com");
// request.Headers.Add(@"あ", @"う"); // NG
request.Headers.TryAddWithoutValidation(@"あ", @"う"); // ヘッダーに付かない
request.Headers.TryAddWithoutValidation(@"hoge1", @"ほげ"); // ヘッダーに付くが、URLエンコードしないとおかしくなる
request.Headers.TryAddWithoutValidation(@"hoge2", new string[] { "1", "2", "3" });

var response = await client.SendAsync(request);
...
}

実際のリクエスト:

GET / HTTP/1.1

hoge1: {R
hoge2: 1, 2, 3
Host: foo.example.com
Connection: Keep-Alive


Basic認証する

普通にAuthorizationヘッダーを送ればよいだけです。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Get, @"http://foo.example.com");
request.Headers.Add(@"Authorization", @"Basic Zm9vOmJhcg==");

var response = await client.SendAsync(request);
...
}

実際のリクエスト:

GET / HTTP/1.1

Authorization: Basic Zm9vOmJhcg==
Host: foo.example.com
Connection: Keep-Alive


レスポンス


ステータスラインを受け取る

単にステータスコードが欲しい場合は、StatusCodeプロパティで取得できます。

using (var client = new HttpClient())

{
var response = await client.GetAysnc(@"http://foo.example.com");
if (response.StatusCode == HttpStatusCode.OK)
{
... 200 OKだった場合の処理 ...
}
}


400以上はエラーなど、範囲で比較したい場合は、intにキャストすれば良いです。

using (var client = new HttpClient())

{
var response = await client.GetAysnc(@"http://foo.example.com");
if ((int) response.StatusCode >= 400)
{
... エラー処理 ...
}
}


200 OK や 403 Forbidden のようなステータスコードに対する文字列が欲しい場合は、ReasonPhraseプロパティで取得できます。

using (var client = new HttpClient())

{
var response = await client.GetAysnc(@"http://foo.example.com");
Console.WriteLine(response.ReasonPhrase);
}


HTTPのバージョンは、Versionプロパティです。

using (var client = new HttpClient())

{
var response = await client.GetAysnc(@"http://foo.example.com");
Console.WriteLine(response.Version);
}


ボディを受け取る

Contentプロパティにボディが入るので、文字列(ReadAsStringAsync())、byte配列(ReadAsByteArrayAsync())、ストリーム(ReadAsStreamAsync)、別ストリームへコピー(CopyToAsync)で取得できます。

using (var client = new HttpClient())

{
var response = await client.PostAysnc(@"http://foo.example.com");
Console.WriteLine(await response.ContentReadAsStringAsync());
}


ヘッダーを受け取る

Headersプロパティで取得できますが、型はHttpResponseHeadersで実体はIEnumerable<string, IEnumerable<string>>となっています。

次のコードは、X-Hogeヘッダーを取得する例です。ValueがstringではなくIEnumerable<string>となっているのは、同一のヘッダー名が複数ある場合(例えば Set-Cookie)があるからです。また、ヘッダー名は大文字・小文字を区別しないので、string.Compare()を使ったほうが良いでしょう。かなり面倒です。

using (var client = new HttpClient())

{
var response = await client.PostAysnc(@"http://foo.example.com");
IEnumerable<string> header = response.Headers.FirstOrDefault(pair => string.Compare(pair.Key, @"X-Hoge") == 0).Value;
}


クッキー


HttpClientにお任せする

HttpClientの同一インスタンスでクッキーを自動的に(ブラウザがやるように)受信・送信をするなら、HttpClientHandlerUseCookieプロパティをtrueにするだけです。

var handler = new HttpClientHandler()

{
UseCookie = true,
};
using (var client = new HttpClient(handler))
{
...
}

クッキー自体はHttpClientHandlerCookieContainerにあります。


自分でクッキーを設定する

HttpClientHandlerCookieContainerでもいいですが、(UseCookie = falseHttpClientインスタンスを毎回生成しているため)自分でリクエストヘッダーにクッキーを設定する方法もあります。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Get, @"http://foo.example.com");
request.Headers.Add(@"Cookie", @"foo=hoge, bar=fuga");

var response = await client.SendAsync(request);
...
}


逆にクッキーをレスポンスから取得する場合も、生ヘッダーから取得できます。

using (var client = new HttpClient())

{
var request = new HttpRequestMessage(HttpMethod.Get, @"http://foo.example.com");
var response = await client.SendAsync(request);
var cookies = response.Headers.FirstOrDefult(pair => string.Compare(pair.Key, @"Set-Cookie", true) == 0).Value;
...
}


その他


コネクションタイムアウトを指定する

Timeoutプロパティで指定します。デフォルトは100秒です。(次の例は、5秒に設定する場合)

using (var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds(5000) })

{
...
}


サーバー証明書の検証をしないようにする

System.Net.ServicePointManager.ServerCertificateValidationCallbackで設定します。

ServicePointManager.ServerCertificateValidationCallback =

new System.Net.Security.RemoteCertificateValidationCallback(
(sender, certification, chain, errors) => true);


自動リダイレクトしないようにする

HttpClientHandlerAllowAutoRedirectプロパティでfalseにします。

var handler = new HttpClientHandler()

{
AllowAutoRedirect = false, // 自動リダイレクトしない
};

using (var client = new HttpClient(handler))
{
...
}


プロキシーを指定する

HttpClientHandlerProxyプロパティで指定します。


  • HTTPプロキシー

var proxy = new WebProxy(@"http://proxy.example.com");

var handler = new HttpClientHandler()
{
Proxy = proxy,
};

using (var client = new HttpClient(handler))
{
...
}


  • HTTPプロキシー+認証あり

var proxy = new WebProxy(@"http://proxy.example.com")

{
Credentials = new NetworkCredential(@"username", @"password");
};

var handler = new HttpClientHandler()
{
Proxy = proxy,
};

using (var client = new HttpClient(handler))
{
...
}


  • システムのプロキシー(≒Internet Explorerのプロキシー設定)を使う

var proxy = WebRequest.GetSystemWebProxy();

var handler = new HttpClientHandler()
{
Proxy = proxy,
};

using (var client = new HttpClient(handler))
{
...
}


Expect: 100-continueを送らないようにする

HttpClientインスタンス毎に指定する場合は、DefaultRequestHeaders.ExpectContinueで設定します。

using (var client = new HttpClient())

{
client.DefaultRequestHeaders.ExpectContinue = false;
...
}

すべてのリクエスト(すべてのドメイン)に対して共通に設定して良いなら、System.Net.ServicePointManagerで設定します。

ServicePointManager.Expect100Continue = false;


Connection: keep-aliveを送らないようにする

Connectionヘッダーを付けないようにすることはできないようなので、Connection: closeを送るようにします。

using (var client = new HttpClient())

{
client.DefaultRequestHeaders.ConnectionClose = true;
...
}

ちなみに System.Net.ServicePointManager.SetTcpKeepAlive()メソッドのkeep-aliveは、別物です。


TLS1.0, 1.1, 1.2を有効にする

デフォルトでTLSの何のバージョンが有効になっているかは、.NETのバージョンに依るため、古い.NETのバージョンではTLS1.1やTLS1.2が有効になっていない場合があります。

ServicePointManager.SecurityProtocol =

SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

古い.NETのバージョンで定数(enum)が定義されていない場合は、数値を直接指定すれば良いです。

ServicePointManager.SecurityProtocol =

SecurityProtocolType.Tls | (SecurityProtocolType)768 | (SecurityProtocolType)3072;


DNSキャッシュをしないようにする

ServicePointManager.DnsRefreshTimeoutプロパティで設定します(ミリ秒)。デフォルトは2分-1を指定すると無制限になります。

ServicePointManager.DnsRefreshTimeout = 10 * 1000; // 10秒に設定

ちなみに、ServicePointConnectionLeaseTimeoutは関係ありません。


まとめ

コネクションプールの話は、これだけで1回分になりそうなので、この記事には含めませんでした。それ以外については、よく使いそうなものは大体書いたつもりです。