재우니의 블로그

 

RESTful 서비스가 유행이라 그런지 애플리케이션에서 HTTP 요청을 던지고 싶을 때가 많은데, HTTP 요청이라고 하면 HttpClient를 떠올리지만, 사용법을 금방 잊어버리는 경우가 많죠. 정리된 사이트도 없는 것 같아서 정리해서 올려봅니다.

 

https://qiita.com/rawr/items/f78a3830d894042f891b

 

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

はじめにRESTfulサービスが流行っているせいか、アプリケーションからHTTPのリクエストを投げたいことが多くなりました。HTTPリクエストと言えばHttpClientですが、使い方をすぐ忘れて…

qiita.com

 

 

Request (요청)

 

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 
}

 


 

HttpRequestMessage를 사용하여 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

 

 

body 보내기 (Content-Type 지정)

 

 

POST나 PUT로 정상적으로 요청 파라미터를 보낼 때는 FormUrlEncodedContent 를 사용하면 되는데... 공백이 +로 인코딩되어 버립니다. (쿼리스트링은 +로, 그 외에는 %20으로 인코딩되는 것이 맞다고 하는데, 대부분의 웹서버는 어느 쪽이든 해석해 줄 것 같습니다).

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"]}

 

 

문자열이 아닌 binary 데이터(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의 인자나 헤더에 설정하면(바로 뒤에 설명) 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
ontent-Length: 2
Connection: Keep-Alive

{}
 
 

 

임의의 헤더 보내기
 
 
header 를 보내고 싶다면 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

 

 

 

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

 

 

응답

 

 

단순히 상태 코드를 원한다면 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()), 바이트 배열(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의 동일한 인스턴스에서 쿠키를 자동으로 (브라우저처럼) 수신 및 전송하려면 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초입니다. (다음 예시는 100초로 설정하는 경우)
 
using (var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds(5000) })
{
    ...
}

 

 

서버 인증서 검증을 하지 않도록 설정하기
 
System.Net.ServicePointManager.ServiceCertificateValidationCallback 에서 설정한다.
 
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))
{
    ...
}

 

 

시스템 프록시(≒ 인터넷 익스플로러 프록시 설정) 사용
 
 
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 사용 설정
 
 
NET 버전에 따라 TLS가 기본적으로 활성화된 버전이 달라지므로, 오래된 .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 속성에서 설정한다(밀리초). 기본값은 1초입니다. -1을 지정하면 무제한이 된다.
 
ServicePointManager.DnsRefreshTimeout = 10 * 1000; // 10秒に設定

 

참고로 ServicePoint의 ConnectionLeaseTimeout은 관계없습니다.
 
 
 

connection pool 에 대한 이야기는 이것만 해도 한 회 분량이 될 것 같아서 이번 글에 포함시키지 않았습니다. 그 외에는 자주 쓸만한 것들은 대충 써놓은 것 같습니다.