C# HttpClient
last modified October 18, 2026
In this article we show how to create HTTP requests with HttpClient
in C#.
The Hypertext Transfer Protocol (HTTP) is an application protocol for distributed, collaborative, hypermedia information systems. HTTP is the foundation of data communication for the World Wide Web.
HttpClient is a class for sending HTTP requests and receiving HTTP
responses from a resource identified by a URI. It is located in the
System.Net.Http namespace. All its request methods are
asynchronous.
An HttpClient instance is meant to be created once and reused
throughout the life of an application, because each instance manages its own
pool of connections. Our short examples create a single client and send one or a
few requests. In applications with dependency injection, such as ASP.NET Core,
clients are created with IHttpClientFactory.
In the examples, we use httpbin.org, a free HTTP request and
response service that returns the details of our requests in JSON, and
example.com, a small page reserved for documentation examples.
HTTP request methods
HTTP defines a set of request methods to indicate the desired action to be performed for a given resource.
- GET - requests a representation of the specified resource
- HEAD - identical to a GET request, but without the response body
- POST - sends data to a resource, often causing state change or side effects
- PUT - creates a resource or replaces an existing resource
- DELETE - deletes the specified resource
- PATCH - performs partial modifications to the resource
- OPTIONS - describes the communication options for the target resource
- CONNECT - starts two-way communications with the requested resource
- TRACE - returns the full HTTP request back for debugging purposes
C# HttpClient status code
HTTP response status codes indicate whether a specific HTTP request has been successfully completed. Responses are grouped in five classes:
- Informational responses (100–199)
- Successful responses (200–299)
- Redirects (300–399)
- Client errors (400–499)
- Server errors (500–599)
using var client = new HttpClient();
string[] urls = ["https://httpbin.org/get", "https://httpbin.org/status/404",
"https://httpbin.org/status/503"];
foreach (var url in urls)
{
using var response = await client.GetAsync(url);
Console.WriteLine($"{(int) response.StatusCode} {response.StatusCode}");
}
The example sends three GET requests and prints the status codes of the responses.
using var response = await client.GetAsync(url);
The GetAsync method sends a GET request to the specified URL as an
asynchronous operation. The await operator suspends the method
until the operation completes and returns its result, an
HttpResponseMessage. The response is disposable; the
using declaration releases the connection.
$ dotnet run 200 OK 404 NotFound 503 ServiceUnavailable
C# HttpClient GET request
The GET method requests a representation of the specified resource.
using var client = new HttpClient();
string content = await client.GetStringAsync("https://example.com");
Console.WriteLine(content);
The example issues a GET request to the example.com website. It
prints the HTML code of the page.
string content = await client.GetStringAsync("https://example.com");
The GetStringAsync method sends a GET request to the specified URL
and returns the response body as a string. Unlike GetAsync, it
throws an HttpRequestException if the status code does not indicate
success.
$ dotnet run
<!doctype html><html lang=en><head><meta charset=utf-8><link rel=icon href=data:,><meta name=viewport content="width=device-width,initial-scale=1"><title>Example Domain</title><style>html{color-scheme:light dark;background:light-dark(#eee,#222)}body{font:16px/1.6 system-ui,sans-serif;max-width:26em;margin:auto;padding:25vh 2em 2em;text-align:center}</style></head><body><p>This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.</p><script src=/s.js></script></body></html>
C# HttpClient HEAD request
The HTTP HEAD method requests the headers that would be returned if the specified resource was requested with an HTTP GET method.
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Head, "https://example.com");
using var response = await client.SendAsync(request);
Console.WriteLine(response.StatusCode);
Console.WriteLine(response.Content.Headers.ContentType);
Console.WriteLine(response.Headers.Contains("Date"));
There is no dedicated method for HEAD requests; we create an
HttpRequestMessage and send it with SendAsync.
Console.WriteLine(response.Content.Headers.ContentType);
Headers that describe the body, such as Content-Type or
Content-Length, are in the Content.Headers
collection. The other headers are in the Headers collection of the
response.
$ dotnet run OK text/html; charset=utf-8 True
C# HttpClient User-Agent
The User-Agent request header is a string that lets servers and
network peers identify the application, operating system, vendor, and/or version
of the requesting user agent. HttpClient does not send this header
by default.
using var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("ZetCodeApp/1.0");
string res = await client.GetStringAsync("https://httpbin.org/user-agent");
Console.WriteLine(res);
The example sets a User-Agent header for all requests of the client. The requested resource returns the client's User-Agent string.
$ dotnet run
{
"user-agent": "ZetCodeApp/1.0"
}
C# HttpClient HttpRequestMessage
The HttpRequestMessage represents a request message. It is used
when we need to set headers for a single request or use a less common HTTP
method.
using System.Net.Http.Json;
using var client = new HttpClient();
using var msg = new HttpRequestMessage(HttpMethod.Get, "https://httpbin.org/headers");
msg.Headers.Add("X-Client-Name", "ZetCode");
msg.Headers.Accept.ParseAdd("application/json");
using var res = await client.SendAsync(msg);
var data = await res.Content.ReadFromJsonAsync<HttpBinResponse>();
Console.WriteLine(data!.Headers["X-Client-Name"]);
Console.WriteLine(data.Headers["Accept"]);
record HttpBinResponse(Dictionary<string, string> Headers);
We build the request message manually and add two headers to it. The httpbin service returns the headers it received.
using var res = await client.SendAsync(msg); var data = await res.Content.ReadFromJsonAsync<HttpBinResponse>();
The message is sent with SendAsync. We read the JSON body of the
response with the ReadFromJsonAsync extension method from the
System.Net.Http.Json namespace.
$ dotnet run ZetCode application/json
C# HttpClient query strings
A query string is a part of the URL which is used to add some data to the
request for the resource. It is a sequence of key/value pairs; it follows the
path and starts with the ? character.
using System.Net.Http.Json;
var values = new Dictionary<string, string>
{
["name"] = "John Doe",
["occupation"] = "gardener & florist"
};
var query = string.Join("&", values.Select(kv =>
$"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"));
var builder = new UriBuilder("https://httpbin.org/get") { Query = query };
Console.WriteLine(builder.Uri.AbsoluteUri);
using var client = new HttpClient();
var res = await client.GetFromJsonAsync<HttpBinResponse>(builder.Uri);
foreach (var (key, value) in res!.Args)
{
Console.WriteLine($"{key}: {value}");
}
record HttpBinResponse(Dictionary<string, string> Args);
The query string is built from a dictionary. The keys and values are encoded
with Uri.EscapeDataString; the & character in the
occupation would otherwise start a new parameter.
Console.WriteLine(builder.Uri.AbsoluteUri);
The AbsoluteUri property returns the encoded URL. The
ToString method of Uri would display the URL in an
unescaped, human-readable form.
$ dotnet run https://httpbin.org/get?name=John%20Doe&occupation=gardener%20%26%20florist name: John Doe occupation: gardener & florist
C# HttpClient timeout
By default, a request times out after 100 seconds. A different timeout can be
set with the Timeout property. When the timeout elapses, the
request is cancelled and a TaskCanceledException is thrown.
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
try
{
await client.GetStringAsync("https://httpbin.org/delay/5");
}
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
{
Console.WriteLine($"Timeout: {e.Message}");
}
The /delay/5 endpoint responds after five seconds; our timeout is
two seconds.
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
A TaskCanceledException is also thrown when the request is
cancelled with a CancellationToken. In case of a timeout, the inner
exception is a TimeoutException.
$ dotnet run Timeout: The request was canceled due to the configured HttpClient.Timeout of 2 seconds elapsing.
C# HttpClient multiple async requests
In the following example, we send multiple GET requests concurrently.
using System.Text.RegularExpressions;
string[] urls = ["https://example.com", "https://httpbin.org",
"https://www.rfc-editor.org", "https://go.dev"];
using var client = new HttpClient();
var rx = new Regex(@"<title>\s*(.+?)\s*</title>", RegexOptions.Singleline);
var tasks = urls.Select(url => client.GetStringAsync(url));
string[] pages = await Task.WhenAll(tasks);
for (int i = 0; i < urls.Length; i++)
{
var title = rx.Match(pages[i]).Groups[1].Value;
Console.WriteLine($"{urls[i]}: {title}");
}
We download the given web pages concurrently and print their titles.
var tasks = urls.Select(url => client.GetStringAsync(url)); string[] pages = await Task.WhenAll(tasks);
Each call to GetStringAsync returns a task that represents a
running request. The Task.WhenAll method returns a task that
completes when all requests complete; awaiting it gives us an array of the
results in the original order. Unlike Task.WaitAll, it does not
block the thread while waiting.
$ dotnet run https://example.com: Example Domain https://httpbin.org: httpbin.org https://www.rfc-editor.org: RFC Editor https://go.dev: The Go Programming Language
C# HttpClient POST request
The HTTP POST method sends data to the server. The type of the body of the
request is indicated by the Content-Type header.
using System.Net.Http.Json;
var person = new Person("John Doe", "gardener");
using var client = new HttpClient();
using var response = await client.PostAsJsonAsync("https://httpbin.org/post", person);
var res = await response.Content.ReadFromJsonAsync<HttpBinResponse>();
Console.WriteLine(res!.Headers["Content-Type"]);
Console.WriteLine(res.Data);
record Person(string Name, string Occupation);
record HttpBinResponse(string Data, Dictionary<string, string> Headers);
In the example, we send a POST request with JSON data to
https://httpbin.org/post.
using var response = await client.PostAsJsonAsync("https://httpbin.org/post", person);
The PostAsJsonAsync method serializes the object to JSON with
System.Text.Json and sends it. No external library is needed.
$ dotnet run
application/json; charset=utf-8
{"name":"John Doe","occupation":"gardener"}
C# HttpClient JSON request
JSON (JavaScript Object Notation) is a lightweight data-interchange format.
This format is easy for humans to read and write and for machines to parse and
generate. The official Internet media type for JSON is
application/json.
using System.Net.Http.Json;
using var client = new HttpClient { BaseAddress = new Uri("https://api.github.com/") };
client.DefaultRequestHeaders.UserAgent.ParseAdd("ZetCodeApp/1.0");
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
var url = "repos/symfony/symfony/contributors?per_page=5";
var contributors = await client.GetFromJsonAsync<List<Contributor>>(url);
contributors?.ForEach(Console.WriteLine);
record Contributor(string Login, int Contributions);
The example sends a GET request to the GitHub API. It finds out the top five contributors of the Symfony framework.
client.DefaultRequestHeaders.UserAgent.ParseAdd("ZetCodeApp/1.0");
The GitHub API rejects requests without the User-Agent header.
var contributors = await client.GetFromJsonAsync<List<Contributor>>(url);
The GetFromJsonAsync method sends the request and deserializes the
JSON array into a list of Contributor records. Only the properties
of the record are read; the other fields of the JSON objects are ignored.
$ dotnet run
Contributor { Login = fabpot, Contributions = 27585 }
Contributor { Login = nicolas-grekas, Contributions = 23339 }
Contributor { Login = xabbuh, Contributions = 4135 }
Contributor { Login = derrabus, Contributions = 2037 }
Contributor { Login = chalasr, Contributions = 1105 }
C# HttpClient POST form data
HTML forms send their data with the
application/x-www-form-urlencoded content type. The
FormUrlEncodedContent class is a container for name/value pairs
encoded with this type.
using System.Net.Http.Json;
var data = new Dictionary<string, string>
{
["name"] = "John Doe",
["occupation"] = "gardener"
};
using var client = new HttpClient();
using var content = new FormUrlEncodedContent(data);
using var response = await client.PostAsync("https://httpbin.org/post", content);
var res = await response.Content.ReadFromJsonAsync<HttpBinResponse>();
foreach (var (key, value) in res!.Form)
{
Console.WriteLine($"{key}: {value}");
}
record HttpBinResponse(Dictionary<string, string> Form);
The example sends a form POST request. The httpbin service returns the parsed form fields.
$ dotnet run name: John Doe occupation: gardener
C# HttpClient PUT and DELETE requests
The PutAsJsonAsync, PatchAsJsonAsync, and
DeleteAsync methods send PUT, PATCH, and DELETE requests.
using System.Net.Http.Json;
using var client = new HttpClient { BaseAddress = new Uri("https://httpbin.org/") };
var user = new User(1, "John Doe", "gardener");
using var r1 = await client.PutAsJsonAsync("put", user);
Console.WriteLine($"PUT: {(int) r1.StatusCode}");
using var r2 = await client.PatchAsJsonAsync("patch", new { occupation = "driver" });
Console.WriteLine($"PATCH: {(int) r2.StatusCode}");
using var r3 = await client.DeleteAsync("delete");
Console.WriteLine($"DELETE: {(int) r3.StatusCode}");
record User(int Id, string Name, string Occupation);
In a REST API, the URL would identify the resource, for instance
users/1. The httpbin service has fixed endpoints for each method.
$ dotnet run PUT: 200 PATCH: 200 DELETE: 200
C# HttpClient download image
The GetByteArrayAsync method sends a GET request to the specified
URL and returns the response body as a byte array.
using var client = new HttpClient();
byte[] imageBytes = await client.GetByteArrayAsync("https://httpbin.org/image/png");
string path = Path.Combine(Path.GetTempPath(), "image.png");
await File.WriteAllBytesAsync(path, imageBytes);
Console.WriteLine($"{imageBytes.Length} bytes written to {Path.GetFileName(path)}");
In the example, we download a PNG image and write it to the temporary folder of the user.
$ dotnet run 8090 bytes written to image.png
This approach keeps the whole file in memory. It is fine for small files; large files should be streamed.
C# HttpClient streaming
Streaming is a method of transmitting data in a continuous stream that can be processed by the receiving computer before the entire file has been received.
using var client = new HttpClient();
var url = "https://www.rfc-editor.org/rfc/rfc9110.txt";
var fname = Path.Combine(Path.GetTempPath(), Path.GetFileName(url));
using var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
await using Stream ms = await resp.Content.ReadAsStreamAsync();
await using FileStream fs = File.Create(fname);
await ms.CopyToAsync(fs);
Console.WriteLine($"{Path.GetFileName(fname)}: {fs.Length} bytes");
In the example, we download the text of the HTTP Semantics specification (RFC 9110) using streaming.
using var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
With the HttpCompletionOption.ResponseHeadersRead option, the
operation completes as soon as the headers are read. The body is not read
yet. Without this option, GetAsync would buffer the whole body in
memory first.
await using Stream ms = await resp.Content.ReadAsStreamAsync();
The ReadAsStreamAsync method returns a stream from which we read
the body as it arrives.
await ms.CopyToAsync(fs);
The data is copied continuously to the file stream.
$ dotnet run rfc9110.txt: 502941 bytes
C# HttpClient Basic authentication
In HTTP protocol, basic access authentication is a method for an HTTP user agent
to provide a user name and password when making a request. The request contains
a header field in the form of Authorization: Basic
<credentials>, where credentials is the Base64 encoding of the user
name and password joined by a single colon.
using System.Net.Http.Headers;
using System.Text;
var userName = "user7";
var passwd = "passwd";
var url = "https://httpbin.org/basic-auth/user7/passwd";
using var client = new HttpClient();
var authToken = Encoding.ASCII.GetBytes($"{userName}:{passwd}");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authToken));
using var result = await client.GetAsync(url);
Console.WriteLine(result.StatusCode);
Console.WriteLine(await result.Content.ReadAsStringAsync());
The example sends credentials to the httpbin.org website.
var url = "https://httpbin.org/basic-auth/user7/passwd";
The URL contains the expected user name and password only because the httpbin service needs to know which credentials to accept. Credentials are never put into URLs in real applications.
$ dotnet run
OK
{
"authenticated": true,
"user": "user7"
}
C# HttpClient proxy
A proxy is an intermediary between a client requesting a resource and the server providing that resource. The proxy is configured on the message handler of the client.
using System.Net;
var handler = new SocketsHttpHandler
{
Proxy = new WebProxy("http://proxy.example.com:8080"),
UseProxy = true
};
using var client = new HttpClient(handler);
string content = await client.GetStringAsync("https://example.com");
Console.WriteLine(content);
The example sends a request through an HTTP proxy. The WebProxy
class also supports SOCKS proxies with URLs such as
socks5://proxy.example.com:1080. Replace the address with a real
proxy server to run the example.
C# HttpClient large download
In the following example, we download a large file: the FreeBSD 15.1 USB memory stick image, which has about 850 MB. The program streams the file to the disk, shows the progress, can resume an interrupted download, and verifies the SHA-256 checksum published by the FreeBSD project.
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
var url = "https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/15.1/FreeBSD-15.1-RELEASE-amd64-memstick.img.xz";
var expectedHash = "ab63c91cd501cedfa7a28020464f2991123ea599c061ca9080c4e453cb0a8e76";
var path = Path.GetFileName(url);
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
using var client = new HttpClient();
long existing = File.Exists(path) ? new FileInfo(path).Length : 0;
using var request = new HttpRequestMessage(HttpMethod.Get, url);
if (existing > 0)
{
request.Headers.Range = new RangeHeaderValue(existing, null);
}
using var resp = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable)
{
Console.WriteLine("The file is already downloaded");
}
else
{
resp.EnsureSuccessStatusCode();
bool resuming = resp.StatusCode == HttpStatusCode.PartialContent;
if (!resuming)
{
existing = 0;
}
long total = existing + (resp.Content.Headers.ContentLength ?? 0);
Console.WriteLine(resuming ? $"Resuming at {existing / 1048576} MB" : $"Downloading {path}");
await using Stream body = await resp.Content.ReadAsStreamAsync();
await using var file = new FileStream(path, resuming ? FileMode.Append : FileMode.Create,
FileAccess.Write, FileShare.None, 81920, useAsync: true);
var buffer = new byte[81920];
long received = existing;
var sw = Stopwatch.StartNew();
TimeSpan lastReport = TimeSpan.Zero;
try
{
int n;
while ((n = await body.ReadAsync(buffer, cts.Token)) > 0)
{
await file.WriteAsync(buffer.AsMemory(0, n));
received += n;
if (sw.Elapsed - lastReport >= TimeSpan.FromMilliseconds(500))
{
lastReport = sw.Elapsed;
ReportProgress(received, total, received - existing, sw.Elapsed);
}
}
ReportProgress(received, total, received - existing, sw.Elapsed);
Console.WriteLine();
}
catch (OperationCanceledException)
{
Console.WriteLine();
Console.WriteLine("Download interrupted; run the program again to resume");
return;
}
}
Console.WriteLine("Verifying SHA-256 checksum...");
await using (var fs = File.OpenRead(path))
{
byte[] hash = await SHA256.HashDataAsync(fs);
string actual = Convert.ToHexStringLower(hash);
Console.WriteLine(actual == expectedHash ? "Checksum OK" : $"Checksum mismatch: {actual}");
}
static void ReportProgress(long received, long total, long downloaded, TimeSpan elapsed)
{
double percent = total > 0 ? received * 100.0 / total : 0;
double speed = downloaded / 1048576.0 / Math.Max(elapsed.TotalSeconds, 0.001);
Console.Write($"\r{received / 1048576,5} / {total / 1048576} MB {percent,5:F1} % {speed,6:F1} MB/s");
}
The file is saved to the current working directory.
long existing = File.Exists(path) ? new FileInfo(path).Length : 0;
using var request = new HttpRequestMessage(HttpMethod.Get, url);
if (existing > 0)
{
request.Headers.Range = new RangeHeaderValue(existing, null);
}
If a part of the file is already on the disk, we ask only for the rest of it
with the Range header. For instance, the
Range: bytes=44125824- header requests the bytes from the given
offset to the end of the file.
using var resp = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
With ResponseHeadersRead, the call returns as soon as the headers
arrive and the body is not buffered in memory. The Timeout of the
client (100 seconds by default) applies only until the headers are received,
so it does not interrupt a long download.
bool resuming = resp.StatusCode == HttpStatusCode.PartialContent;
A server that supports ranges answers with 206 Partial Content. A server that does not support them sends the whole file with 200 OK; in that case, we start from the beginning. If the file is already complete, the requested range lies past its end and the server answers with 416 Range Not Satisfiable.
await using var file = new FileStream(path, resuming ? FileMode.Append : FileMode.Create,
FileAccess.Write, FileShare.None, 81920, useAsync: true);
When resuming, we append to the existing file.
while ((n = await body.ReadAsync(buffer, cts.Token)) > 0)
{
await file.WriteAsync(buffer.AsMemory(0, n));
received += n;
...
}
Instead of CopyToAsync, we copy the data in a loop, so that we can
count the received bytes. The progress is reported at most twice per second;
the \r character returns the cursor to the start of the line, so
the line is overwritten.
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
cts.Cancel();
};
When we press Ctrl+C, the reading is cancelled and the file stream is disposed properly, so all received data stays on the disk.
byte[] hash = await SHA256.HashDataAsync(fs); string actual = Convert.ToHexStringLower(hash);
Finally, we compute the SHA-256 hash of the file and compare it with the
expected value from the CHECKSUM.SHA256 file of the release.
$ dotnet run Downloading FreeBSD-15.1-RELEASE-amd64-memstick.img.xz 41 / 853 MB 4.9 % 4.4 MB/s Download interrupted; run the program again to resume
We interrupt the download with Ctrl+C. When we run the program again, it continues where it stopped and verifies the checksum at the end.
C# HttpClient SocketsHttpHandler
HttpClient sends its requests through a message handler. The
default handler is SocketsHttpHandler; by creating it ourselves,
we can configure the connections.
using System.Net;
using System.Net.Sockets;
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30),
MaxConnectionsPerServer = 10,
ConnectTimeout = TimeSpan.FromSeconds(5),
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = async (context, token) =>
{
IPAddress[] addresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host,
AddressFamily.InterNetwork, token);
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
try
{
await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, token);
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
};
using var ipv4Client = new HttpClient(handler);
using var defaultClient = new HttpClient();
var url = "https://api64.ipify.org";
string ip1 = await defaultClient.GetStringAsync(url);
string ip2 = await ipv4Client.GetStringAsync(url);
Console.WriteLine($"default client: {IPAddress.Parse(ip1).AddressFamily}");
Console.WriteLine($"IPv4 client: {IPAddress.Parse(ip2).AddressFamily}");
We configure the connection pool and force the client to use IPv4.
PooledConnectionLifetime = TimeSpan.FromMinutes(2), PooledConnectionIdleTimeout = TimeSpan.FromSeconds(30), MaxConnectionsPerServer = 10, ConnectTimeout = TimeSpan.FromSeconds(5), AutomaticDecompression = DecompressionMethods.All,
Connections are kept in a pool and reused. The
PooledConnectionLifetime limits how long a connection is used, so
that a long-lived client picks up DNS changes. This is the recommended setting
for an HttpClient that lives for the whole application. Idle
connections are closed after 30 seconds, and at most ten connections are open
to one server. Responses compressed with gzip, deflate, or Brotli are
decompressed automatically.
IPAddress[] addresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host,
AddressFamily.InterNetwork, token);
In the ConnectCallback, we open the connection ourselves. We
resolve only the IPv4 addresses of the host and connect to them.
var url = "https://api64.ipify.org";
The ipify service returns the public IP address from which the request came. We print only its address family.
$ dotnet run default client: InterNetworkV6 IPv4 client: InterNetwork
On a computer with IPv6 connectivity, the default client prefers IPv6. Without IPv6, both clients use IPv4.
C# HttpClient cookies
The handler stores cookies sent by servers in a CookieContainer
and sends them back with subsequent requests.
using System.Net;
var cookies = new CookieContainer();
cookies.Add(new Cookie("session", "abc123", "/", "httpbin.org"));
var handler = new SocketsHttpHandler { CookieContainer = cookies };
using var client = new HttpClient(handler);
using var resp = await client.GetAsync("https://httpbin.org/cookies/set?theme=dark&lang=en");
Console.WriteLine($"{(int) resp.StatusCode} {resp.RequestMessage?.RequestUri}");
Console.WriteLine(await resp.Content.ReadAsStringAsync());
foreach (Cookie c in cookies.GetCookies(new Uri("https://httpbin.org")))
{
Console.WriteLine($"{c.Name} = {c.Value}");
}
We add one cookie manually; the httpbin service sets two more.
using var resp = await client.GetAsync("https://httpbin.org/cookies/set?theme=dark&lang=en");
The /cookies/set endpoint sets the cookies from the query string
and redirects to /cookies, which returns the cookies it received.
HttpClient follows the redirect automatically; the
RequestMessage property of the response shows the final URL.
foreach (Cookie c in cookies.GetCookies(new Uri("https://httpbin.org")))
{
Console.WriteLine($"{c.Name} = {c.Value}");
}
We list the cookies stored for the site.
$ dotnet run
200 https://httpbin.org/cookies
{
"cookies": {
"lang": "en",
"session": "abc123",
"theme": "dark"
}
}
session = abc123
theme = dark
lang = en
C# HttpClient stub handler for testing
Code that uses HttpClient can be tested without a network. We give
the client a custom HttpMessageHandler that returns prepared
responses and records the requests.
using System.Net;
using System.Net.Http.Json;
using System.Text;
var stub = new StubHandler(HttpStatusCode.OK, """{ "city": "Bratislava", "temperature": 21.5 }""");
using var http = new HttpClient(stub) { BaseAddress = new Uri("https://weather.example.com/") };
var weather = new WeatherClient(http);
double temp = await weather.GetTemperatureAsync("Bratislava");
Console.WriteLine($"temperature: {temp}");
Console.WriteLine($"request: {stub.LastRequest?.Method} {stub.LastRequest?.RequestUri}");
var failing = new StubHandler(HttpStatusCode.ServiceUnavailable, "");
var weather2 = new WeatherClient(new HttpClient(failing) { BaseAddress = http.BaseAddress });
try
{
await weather2.GetTemperatureAsync("Bratislava");
}
catch (HttpRequestException e)
{
Console.WriteLine($"error: {e.StatusCode}");
}
class WeatherClient(HttpClient client)
{
public async Task<double> GetTemperatureAsync(string city)
{
var data = await client.GetFromJsonAsync<WeatherData>($"weather?city={Uri.EscapeDataString(city)}");
return data?.Temperature ?? double.NaN;
}
}
record WeatherData(string City, double Temperature);
class StubHandler(HttpStatusCode status, string json) : HttpMessageHandler
{
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken token)
{
LastRequest = request;
var response = new HttpResponseMessage(status)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
return Task.FromResult(response);
}
}
The WeatherClient class gets its HttpClient through
the constructor, so we can pass it a client with a stub handler.
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken token)
Every request made by HttpClient ends in the
SendAsync method of its handler. Our handler does not send
anything; it stores the request and returns the response given in the
constructor.
var failing = new StubHandler(HttpStatusCode.ServiceUnavailable, "");
We can also simulate errors. The GetFromJsonAsync method throws an
HttpRequestException for an unsuccessful status code.
$ dotnet run temperature: 21.5 request: GET https://weather.example.com/weather?city=Bratislava error: ServiceUnavailable
In a test project, the same stub is used in the test methods; see the C# MSTest and C# NUnit articles.
Source
Make HTTP requests with the HttpClient class
In this article we have used C# HttpClient to create HTTP
requests.
Author
List all C# tutorials.