ZetCode

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.

C# HttpClient status code

HTTP response status codes indicate whether a specific HTTP request has been successfully completed. Responses are grouped in five classes:

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Note: The credentials are not encrypted; therefore, HTTP basic authentication must be used with the HTTPS protocol.
Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.