346
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

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

はじめに

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.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.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回分になりそうなので、この記事には含めませんでした。それ以外については、よく使いそうなものは大体書いたつもりです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
346
Help us understand the problem. What are the problem?