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

# はじめに

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

# リクエスト

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

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

```csharp
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がありません](https://msdn.microsoft.com/ja-jp/library/system.net.http.httpmethod(v=vs.118).aspx)。（WebDAV系のメソッドもありません）

---

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

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

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

## クエリパラメータを送る

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

```csharp
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でエンコードされる）

```http
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サーバーはどちらでも解釈してくれると思います)

```csharp
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が自動的に設定される）

```http
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` になります。

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
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`コンストラクターの第３引数で指定します。

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
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は次のように設定します。

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
POST / HTTP/1.1
Content-Type: text/hoge
Host: foo.example.com
Content-Length: 15
Connection: Keep-Alive

...............
```

---

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

```csharp
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`の第３引数やヘッダーに設定（すぐ後で説明します）すると、`System.FormatException`が発生してしまいます。`ByteArrayContent`を使いつつ、`HttpContent.Headers.TryAddWithoutValidation()`で無理矢理設定します。

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
POST / HTTP/1.1
Content-Type: hogehoge
Host: foo.example.com
Content-Length: 2
Connection: Keep-Alive

{}
```

## 任意のヘッダーを送る

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

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
GET / HTTP/1.1
X-Hoge: foo
Host: foo.example.com
Connection: Keep-Alive
```

---

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

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
GET / HTTP/1.1
hoge1: {R
hoge2: 1, 2, 3
Host: foo.example.com
Connection: Keep-Alive
```

## Basic認証する

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

```csharp
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);
    ...
}
```

実際のリクエスト：

```http
GET / HTTP/1.1
Authorization: Basic Zm9vOmJhcg==
Host: foo.example.com
Connection: Keep-Alive
```

# レスポンス

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

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

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

```

---

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

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

```

---

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

```csharp
using (var client = new HttpClient())
{
    var response = await client.GetAsync(@"http://foo.example.com");
    Console.WriteLine(response.ReasonPhrase);
}
```

---

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

```csharp
using (var client = new HttpClient())
{
    var response = await client.GetAsync(@"http://foo.example.com");
    Console.WriteLine(response.Version);
}
```

## ボディを受け取る

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

```csharp
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()`を使ったほうが良いでしょう。かなり面倒です。


```csharp
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`にするだけです。

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

クッキー自体は`HttpClientHandler`の`CookieContainer`にあります。

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

`HttpClientHandler`の`CookieContainer`でもいいですが、（`UseCookie = false`や`HttpClient`インスタンスを毎回生成しているため）自分でリクエストヘッダーにクッキーを設定する方法もあります。

```csharp
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);
    ...
}
```

---

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

```csharp
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秒](https://msdn.microsoft.com/en-us/library/system.net.http.httpclient.timeout(v=vs.110).aspx)です。（次の例は、５秒に設定する場合）

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

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

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

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

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

`HttpClientHandler`の`AllowAutoRedirect`プロパティで`false`にします。

```csharp
var handler = new HttpClientHandler()
{
    AllowAutoRedirect = false, // 自動リダイレクトしない
};

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

## プロキシーを指定する

`HttpClientHandler`の`Proxy`プロパティで指定します。

- HTTPプロキシー

```csharp
var proxy = new WebProxy(@"http://proxy.example.com");
var handler = new HttpClientHandler()
{
    Proxy = proxy, 
};

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

- HTTPプロキシー＋認証あり

```csharp
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のプロキシー設定）を使う

```csharp
var proxy = WebRequest.GetSystemWebProxy();
var handler = new HttpClientHandler()
{
    Proxy = proxy, 
};

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

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

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

```csharp
using (var client = new HttpClient())
{
    client.DefaultRequestHeaders.ExpectContinue = false;
    ...
}
```

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

```csharp
ServicePointManager.Expect100Continue = false;
```

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

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

```csharp
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が有効になっていない場合があります。

```csharp
ServicePointManager.SecurityProtocol =
    SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
```

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

```csharp
ServicePointManager.SecurityProtocol =
    SecurityProtocolType.Tls | (SecurityProtocolType)768 | (SecurityProtocolType)3072;
```

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

`ServicePointManager.DnsRefreshTimeout`プロパティで設定します（ミリ秒）。[デフォルトは２分](https://msdn.microsoft.com/ja-jp/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx)。`-1`を指定すると無制限になります。

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

ちなみに、`ServicePoint`の`ConnectionLeaseTimeout`は関係ありません。

# まとめ

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