HttpClient

Send HTTP requests and read responses with HttpClient, and avoid the port exhaustion caused by one client per call.

Syntaxusing HttpResponseMessage response = await client.GetAsync("items/42");

A service usually needs data that lives somewhere else: a payment provider, a weather feed, another team's API. HttpClient is the type in the base class library that makes that call. It sends an HTTP request and hands back an HTTP response. A request carries a method (GET to read something, POST to create something), a URL, headers (name and value pairs of metadata), and sometimes a body. A response carries a status code, headers and a body.

Sending one

GetAsync returns an HttpResponseMessage. Its StatusCode says what happened. EnsureSuccessStatusCode() throws an HttpRequestException when the status is not a success code, so you do not have to check by hand. The body hangs off Content and ReadAsStringAsync() reads it.

Settings that apply to every call live on the client itself: BaseAddress, Timeout (100 seconds unless you change it) and DefaultRequestHeaders.

The short form for JSON

The System.Net.Http.Json namespace adds GetFromJsonAsync<T>. It is a GetAsync, a status check and a deserialize rolled into one call, nothing more. The examples below show both spellings of the same work, because you will meet both.

One client, many calls

Each HttpClient owns a pool of TCP connections. Creating a new one per call, especially inside a using, opens a fresh pool every time, and the operating system holds the closed ports for a while afterwards. Under load the machine runs out of ports and calls start failing. Reuse one client instead: either a long-lived instance with SocketsHttpHandler.PooledConnectionLifetime set, or the factory in the next lesson.

Example

Example · csharp
using System.Net.Http.Headers;

// One client, configured once, reused for every call it makes.
using HttpClient client = new();
client.BaseAddress = new Uri("https://example.com/");
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));

using HttpResponseMessage response = await client.GetAsync("/");

// StatusCode is an enum; casting it to int gives the number on the wire.
Console.WriteLine($"status: {(int)response.StatusCode} {response.StatusCode}");

// Throws HttpRequestException for 4xx and 5xx, so the lines below only run on success.
response.EnsureSuccessStatusCode();

string body = await response.Content.ReadAsStringAsync();
bool hasTitle = body.Contains("<title>Example Domain</title>");
Console.WriteLine($"has title: {hasTitle}");

// Output:
// status: 200 OK
// has title: True

When to use it

  • Read a price list from a partner's JSON API and turn it into your own objects.
  • Post an order to a payment provider and inspect the status code it returns.
  • Replace a per-request `new HttpClient()` that is exhausting ports in production.
  • Give an outbound call a two-second budget so a slow dependency cannot stall your request.

More examples

The JSON shorthand and the longhand it stands for

GetFromJsonAsync is not magic and it is not a different mechanism. It performs the GET, calls EnsureSuccessStatusCode, and deserializes the body with System.Text.Json. Reach for the short form in everyday code, and for the long form when you need the response itself, for example to read a header or to treat a 404 as a legitimate answer rather than an exception.

Example · csharp
using System.Net.Http.Json;

using HttpClient client = new();
client.BaseAddress = new Uri("https://api.example.com/");

// Shorthand: one call does the GET, the status check and the deserialize.
Product? shortForm = await client.GetFromJsonAsync<Product>("products/42");

// Longhand: exactly the same three steps, written out.
using HttpResponseMessage response = await client.GetAsync("products/42");
response.EnsureSuccessStatusCode();
Product? longForm = await response.Content.ReadFromJsonAsync<Product>();

Console.WriteLine(shortForm?.Name);
Console.WriteLine(longForm?.Name);

record Product(int Id, string Name);

POST a body and read what came back

Use this shape whenever you create something in another service. Note the order: check the status codes you have a plan for first, then call EnsureSuccessStatusCode for everything else. Checking afterwards is too late, because EnsureSuccessStatusCode has already thrown.

Example · csharp
using System.Net.Http.Json;

using HttpClient client = new();
client.BaseAddress = new Uri("https://api.example.com/");

// PostAsJsonAsync serialises the object to JSON and sets Content-Type for you.
using HttpResponseMessage response =
    await client.PostAsJsonAsync("products", new { name = "kettle", priceCents = 2995 });

if (response.StatusCode == System.Net.HttpStatusCode.Conflict)
{
    // A 409 is an answer, not a transport failure. Handle it instead of throwing.
    Console.WriteLine("a product with that name already exists");
    return;
}

response.EnsureSuccessStatusCode();

Product? created = await response.Content.ReadFromJsonAsync<Product>();
Console.WriteLine($"created id {created?.Id}");

record Product(int Id, string Name);

A long-lived client that still notices DNS changes

This is the supported alternative to IHttpClientFactory for a client you own outright. HttpClient resolves a host name only when it opens a connection and ignores the DNS record's time to live, so a client that keeps its connections open forever can keep talking to a machine that has been replaced. Setting PooledConnectionLifetime bounds how long that can last.

Example · csharp
// A client per call exhausts TCP ports. A static client that never replaces its
// connections stops following DNS changes. PooledConnectionLifetime fixes both.
static class Downstream
{
    private static readonly SocketsHttpHandler _handler = new()
    {
        // Close and reopen each pooled connection after two minutes,
        // which forces a fresh DNS lookup.
        PooledConnectionLifetime = TimeSpan.FromMinutes(2)
    };

    public static readonly HttpClient Client = new(_handler)
    {
        BaseAddress = new Uri("https://api.example.com/"),
        Timeout = TimeSpan.FromSeconds(10)
    };
}

Discussion

  • Be the first to comment on this lesson.