RESTful 서비스가 유행이라 그런지 애플리케이션에서 HTTP 요청을 던지고 싶을 때가 많은데, HTTP 요청이라고 하면 HttpClient를 떠올리지만, 사용법을 금방 잊어버리는 경우가 많죠. 정리된 사이트도 없는 것 같아서 정리해서 올려봅니다.
https://qiita.com/rawr/items/f78a3830d894042f891b
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
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);
...
}
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
{}
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
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)
{
... エラー処理 ...
}
}
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;
...
}
기타
using (var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds(5000) })
{
...
}
ServicePointManager.ServerCertificateValidationCallback =
new System.Net.Security.RemoteCertificateValidationCallback(
(sender, certification, chain, errors) => true);
var handler = new HttpClientHandler()
{
AllowAutoRedirect = false, // 自動リダイレクトしない
};
using (var client = new HttpClient(handler))
{
...
}
var proxy = new WebProxy(@"http://proxy.example.com");
var handler = new HttpClientHandler()
{
Proxy = proxy,
};
using (var client = new HttpClient(handler))
{
...
}
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))
{
...
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.ExpectContinue = false;
...
}
ServicePointManager.Expect100Continue = false;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.ConnectionClose = true;
...
}
ServicePointManager.SecurityProtocol =
SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
NET 버전에서 상수(enum)가 정의되어 있지 않은 경우, 숫자를 직접 지정하면 된다.
ServicePointManager.SecurityProtocol =
SecurityProtocolType.Tls | (SecurityProtocolType)768 | (SecurityProtocolType)3072;
ServicePointManager.DnsRefreshTimeout = 10 * 1000; // 10秒に設定
connection pool 에 대한 이야기는 이것만 해도 한 회 분량이 될 것 같아서 이번 글에 포함시키지 않았습니다. 그 외에는 자주 쓸만한 것들은 대충 써놓은 것 같습니다.
ADO.NET 및 Entity Framework & dapper 의 DbType.AnsiString 조사하기 (4) | 2024.02.27 |
---|---|
EPPlus : Please set the ExcelPackage.LicenseContext property 대처방안 (4) | 2024.02.21 |
c# : Dependency Inversion Principle (DIP) - 종속성 반전 원칙 (0) | 2024.01.19 |
c# : Interface Segregation Principle (ISP) - 인터페이스 분리 원칙 (0) | 2024.01.19 |
C# : Liskov Substitution Principle (LSP) - 리스코프 대체(치환) 원칙 (0) | 2024.01.19 |