はじめに
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/json
や application/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.GetAsync(@"http://foo.example.com");
if (response.StatusCode == HttpStatusCode.OK)
{
... 200 OKだった場合の処理 ...
}
}
400以上はエラーなど、範囲で比較したい場合は、intにキャストすれば良いです。
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://foo.example.com");
if ((int) response.StatusCode >= 400)
{
... エラー処理 ...
}
}
200 OK や 403 Forbidden のようなステータスコードに対する文字列が欲しい場合は、ReasonPhrase
プロパティで取得できます。
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://foo.example.com");
Console.WriteLine(response.ReasonPhrase);
}
HTTPのバージョンは、Version
プロパティです。
using (var client = new HttpClient())
{
var response = await client.GetAsync(@"http://foo.example.com");
Console.WriteLine(response.Version);
}
ボディを受け取る
Content
プロパティにボディが入るので、文字列(ReadAsStringAsync()
)、byte配列(ReadAsByteArrayAsync()
)、ストリーム(ReadAsStreamAsync
)、別ストリームへコピー(CopyToAsync
)で取得できます。
using (var client = new HttpClient())
{
var response = await client.PostAsync(@"http://foo.example.com");
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
ヘッダーを受け取る
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.PostAsync(@"http://foo.example.com");
IEnumerable<string> header = response.Headers.FirstOrDefault(pair => string.Compare(pair.Key, @"X-Hoge") == 0).Value;
}
クッキー
HttpClientにお任せする
HttpClient
の同一インスタンスでクッキーを自動的に(ブラウザがやるように)受信・送信をするなら、HttpClientHandler
のUseCookie
プロパティをtrue
にするだけです。
var handler = new HttpClientHandler()
{
UseCookie = true,
};
using (var client = new HttpClient(handler))
{
...
}
クッキー自体はHttpClientHandler
のCookieContainer
にあります。
自分でクッキーを設定する
HttpClientHandler
のCookieContainer
でもいいですが、(UseCookie = false
やHttpClient
インスタンスを毎回生成しているため)自分でリクエストヘッダーにクッキーを設定する方法もあります。
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);
自動リダイレクトしないようにする
HttpClientHandler
のAllowAutoRedirect
プロパティでfalse
にします。
var handler = new HttpClientHandler()
{
AllowAutoRedirect = false, // 自動リダイレクトしない
};
using (var client = new HttpClient(handler))
{
...
}
プロキシーを指定する
HttpClientHandler
のProxy
プロパティで指定します。
- 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秒に設定
ちなみに、ServicePoint
のConnectionLeaseTimeout
は関係ありません。
まとめ
コネクションプールの話は、これだけで1回分になりそうなので、この記事には含めませんでした。それ以外については、よく使いそうなものは大体書いたつもりです。