Make HTTP requests with the HttpClient class

In this article, you'll learn how to make HTTP requests and handle responses with the HttpClient class.

Important

All of the example HTTP requests target one of the following URLs:

HTTP endpoints commonly return JavaScript Object Notation (JSON) data, but not always. For convenience, the optional System.Net.Http.Json NuGet package provides several extension methods for HttpClient and HttpContent that perform automatic serialization and deserialization using System.Text.Json. The examples that follow call attention to places where these extensions are available.

Tip

All of the source code from this article is available in the GitHub: .NET Docs repository.

Create an HttpClient

Most of the following examples reuse the same HttpClient instance, and therefore only need to be configured once. To create an HttpClient, use the HttpClient class constructor. For more information, see Guidelines for using HttpClient.

using HttpClient todoClient = new()
{
    BaseAddress = new Uri("https://jsonplaceholder.typicode.com")
};

The preceding code:

This HttpClient instance will always use the base address when making subsequent requests. To apply additional configuration consider:

Tip

Alternatively, you can create HttpClient instances using a factory-pattern approach that allows you to configure any number of clients and consume them as dependency injection services. For more information, see IHttpClientFactory with .NET.

Make an HTTP request

To make an HTTP request, you call any of the following APIs:

HTTP verb API
GET HttpClient.GetAsync
GET HttpClient.GetByteArrayAsync
GET HttpClient.GetStreamAsync
GET HttpClient.GetStringAsync
POST HttpClient.PostAsync
PUT HttpClient.PutAsync
PATCH HttpClient.PatchAsync
DELETE HttpClient.DeleteAsync
USER SPECIFIED HttpClient.SendAsync

A USER SPECIFIED request indicates that the SendAsync method accepts any valid HttpMethod.

Warning

Making HTTP requests is considered network I/O-bound work. While there is a synchronous HttpClient.Send method, it is recommended to use the asynchronous APIs instead, unless you have good reason not to.

HTTP content

The HttpContent type is used to represent an HTTP entity body and corresponding content headers. For HTTP verbs (or request methods) that require a body, POST, PUT, and PATCH, you use the HttpContent class to specify the body of the request. Most examples show how to prepare the StringContent subclass with a JSON payload, but additional subclasses exist for different content (MIME) types.

The HttpContent class is also used to represent the response body of the HttpResponseMessage, accessible on the HttpResponseMessage.Content property.

HTTP Get

A GET request shouldn't send a body and is used (as the verb indicates) to retrieve (or get) data from a resource. To make an HTTP GET request, given an HttpClient and a URI, use the HttpClient.GetAsync method:

static async Task GetAsync(HttpClient client)
{
    using HttpResponseMessage response = await client.GetAsync("todos/3");
    
    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();
    
    var jsonResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{jsonResponse}\n");

    // Expected output:
    //   GET https://jsonplaceholder.typicode.com/todos/3 HTTP/ 1.1
    //   {
    //     "userId": 1,
    //     "id": 3,
    //     "title": "fugiat veniam minus",
    //     "completed": false
    //   }
}

The preceding code:

  • Makes a GET request to "https://jsonplaceholder.typicode.com/todos/3".
  • Ensures that the response is successful.
  • Writes the request details to the console.
  • Reads the response body as a string.
  • Writes the JSON response body to the console.

The WriteRequestToConsole is a custom extension method that isn't part of the framework, but if you're curious how it's written, consider the following C# code:

static class HttpResponseMessageExtensions
{
    internal static void WriteRequestToConsole(this HttpResponseMessage response)
    {
        if (response is null)
        {
            return;
        }

        var request = response.RequestMessage;
        Write($"{request?.Method} ");
        Write($"{request?.RequestUri} ");
        WriteLine($"HTTP/{request?.Version}");        
    }
}

HTTP Get from JSON

The https://jsonplaceholder.typicode.com/todos endpoint returns a JSON array of "todo" objects. Their JSON structure resembles the following:

[
  {
    "userId": 1,
    "id": 1,
    "title": "example title",
    "completed": false
  },
  {
    "userId": 1,
    "id": 2,
    "title": "another example title",
    "completed": true
  },
]

The C# Todo object is defined as follows:

public record class Todo(
    int? UserId = null,
    int? Id = null,
    string? Title = null,
    bool? Completed = null);

It's a record class type, with optional Id, Title, Completed, and UserId properties. For more information on the record type, see Introduction to record types in C#. To automatically deserialize GET requests into strongly typed C# object, use the GetFromJsonAsync extension method that's part of the System.Net.Http.Json NuGet package.

static async Task GetFromJsonAsync(HttpClient client)
{
    var todos = await client.GetFromJsonAsync<List<Todo>>(
        "todos?userId=1&completed=false");

    WriteLine("GET https://jsonplaceholder.typicode.com/todos?userId=1&completed=false HTTP/1.1");
    todos?.ForEach(WriteLine);
    WriteLine();

    // Expected output:
    //   GET https://jsonplaceholder.typicode.com/todos?userId=1&completed=false HTTP/1.1
    //   Todo { UserId = 1, Id = 1, Title = delectus aut autem, Completed = False }
    //   Todo { UserId = 1, Id = 2, Title = quis ut nam facilis et officia qui, Completed = False }
    //   Todo { UserId = 1, Id = 3, Title = fugiat veniam minus, Completed = False }
    //   Todo { UserId = 1, Id = 5, Title = laboriosam mollitia et enim quasi adipisci quia provident illum, Completed = False }
    //   Todo { UserId = 1, Id = 6, Title = qui ullam ratione quibusdam voluptatem quia omnis, Completed = False }
    //   Todo { UserId = 1, Id = 7, Title = illo expedita consequatur quia in, Completed = False }
    //   Todo { UserId = 1, Id = 9, Title = molestiae perspiciatis ipsa, Completed = False }
    //   Todo { UserId = 1, Id = 13, Title = et doloremque nulla, Completed = False }
    //   Todo { UserId = 1, Id = 18, Title = dolorum est consequatur ea mollitia in culpa, Completed = False }
}

In the preceding code:

  • A GET request is made to "https://jsonplaceholder.typicode.com/todos?userId=1&completed=false".
    • The query string represents the filtering criteria for the request.
  • The response is automatically deserialized into a List<Todo> when successful.
  • The request details are written to the console, along with each Todo object.

HTTP Post

A POST request sends data to the server for processing. The Content-Type header of the request signifies what MIME type the body is sending. To make an HTTP POST request, given an HttpClient and a URI, use the HttpClient.PostAsync method:

static async Task PostAsync(HttpClient client)
{
    using StringContent jsonContent = new(
        JsonSerializer.Serialize(new
        {
            userId = 77,
            id = 1,
            title = "write code sample",
            completed = false
        }),
        Encoding.UTF8,
        "application/json");

    using HttpResponseMessage response = await client.PostAsync(
        "todos",
        jsonContent);

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();
    
    var jsonResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{jsonResponse}\n");

    // Expected output:
    //   POST https://jsonplaceholder.typicode.com/todos HTTP/1.1
    //   {
    //     "userId": 77,
    //     "id": 201,
    //     "title": "write code sample",
    //     "completed": false
    //   }
}

The preceding code:

  • Prepares a StringContent instance with the JSON body of the request (MIME type of "application/json").
  • Makes a POST request to "https://jsonplaceholder.typicode.com/todos".
  • Ensures that the response is successful, and writes the request details to the console.
  • Writes the response body as a string to the console.

HTTP Post as JSON

To automatically serialize POST request arguments and deserialize responses into strongly-typed C# objects, use the PostAsJsonAsync extension method that's part of the System.Net.Http.Json NuGet package.

static async Task PostAsJsonAsync(HttpClient client)
{
    using HttpResponseMessage response = await client.PostAsJsonAsync(
        "todos", 
        new Todo(UserId: 9, Id: 99, Title: "Show extensions", Completed: false));

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    var todo = await response.Content.ReadFromJsonAsync<Todo>();
    WriteLine($"{todo}\n");

    // Expected output:
    //   POST https://jsonplaceholder.typicode.com/todos HTTP/1.1
    //   Todo { UserId = 9, Id = 201, Title = Show extensions, Completed = False }
}

The preceding code:

  • Serializes the Todo instance as JSON, and makes a POST request to "https://jsonplaceholder.typicode.com/todos".
  • Ensures that the response is successful, and writes the request details to the console.
  • Deserializes the response body into a Todo instance, and writes the Todo to the console.

HTTP Put

The PUT request method either replaces an existing resource or creates a new one using request body payload. To make an HTTP PUT request, given an HttpClient and a URI, use the HttpClient.PutAsync method:

static async Task PutAsync(HttpClient client)
{
    using StringContent jsonContent = new(
        JsonSerializer.Serialize(new 
        {
            userId = 1,
            id = 1,
            title = "foo bar",
            completed = false
        }),
        Encoding.UTF8,
        "application/json");

    using HttpResponseMessage response = await client.PutAsync(
        "todos/1",
        jsonContent);

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();
    
    var jsonResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{jsonResponse}\n");

    // Expected output:
    //   PUT https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
    //   {
    //     "userId": 1,
    //     "id": 1,
    //     "title": "foo bar",
    //     "completed": false
    //   }
}

The preceding code:

  • Prepares a StringContent instance with the JSON body of the request (MIME type of "application/json").
  • Makes a PUT request to "https://jsonplaceholder.typicode.com/todos/1".
  • Ensures that the response is successful, and writes the request details and JSON response body to the console.

HTTP Put as JSON

To automatically serialize PUT request arguments and deserialize responses into strongly typed C# objects, use the PutAsJsonAsync extension method that's part of the System.Net.Http.Json NuGet package.

static async Task PutAsJsonAsync(HttpClient client)
{
    using HttpResponseMessage response = await client.PutAsJsonAsync(
        "todos/5",
        new Todo(Title: "partially update todo", Completed: true));

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    var todo = await response.Content.ReadFromJsonAsync<Todo>();
    WriteLine($"{todo}\n");

    // Expected output:
    //   PUT https://jsonplaceholder.typicode.com/todos/5 HTTP/1.1
    //   Todo { UserId = , Id = 5, Title = partially update todo, Completed = True }
}

The preceding code:

  • Serializes the Todo instance as JSON, and makes a PUT request to "https://jsonplaceholder.typicode.com/todos/5".
  • Ensures that the response is successful, and writes the request details to the console.
  • Deserializes the response body into a Todo instance, and writes the Todo to the console.

HTTP Patch

The PATCH request is a partial update to an existing resource. It won't create a new resource, and it's not intended to replace an existing resource. Instead, it updates a resource only partially. To make an HTTP PATCH request, given an HttpClient and a URI, use the HttpClient.PatchAsync method:

static async Task PatchAsync(HttpClient client)
{
    using StringContent jsonContent = new(
        JsonSerializer.Serialize(new
        {
            completed = true
        }),
        Encoding.UTF8,
        "application/json");

    using HttpResponseMessage response = await client.PatchAsync(
        "todos/1",
        jsonContent);

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    var jsonResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{jsonResponse}\n");

    // Expected output
    //   PATCH https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
    //   {
    //     "userId": 1,
    //     "id": 1,
    //     "title": "delectus aut autem",
    //     "completed": true
    //   }
}

The preceding code:

  • Prepares a StringContent instance with the JSON body of the request (MIME type of "application/json").
  • Makes a PATCH request to "https://jsonplaceholder.typicode.com/todos/1".
  • Ensures that the response is successful, and writes the request details and JSON response body to the console.

No extension methods exist for PATCH requests in the System.Net.Http.Json NuGet package.

HTTP Delete

A DELETE request deletes an existing resource. A DELETE request is idempotent but not safe, meaning multiple DELETE requests to the same resources yield the same result, but the request will affect the state of the resource. To make an HTTP DELETE request, given an HttpClient and a URI, use the HttpClient.DeleteAsync method:

static async Task DeleteAsync(HttpClient client)
{
    using HttpResponseMessage response = await client.DeleteAsync("todos/1");
    
    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    var jsonResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{jsonResponse}\n");

    // Expected output
    //   DELETE https://jsonplaceholder.typicode.com/todos/1 HTTP/1.1
    //   {}
}

The preceding code:

  • Makes a DELETE request to "https://jsonplaceholder.typicode.com/todos/1".
  • Ensures that the response is successful, and writes the request details to the console.

Tip

The response to a DELETE request (just like a PUT request) may or may not include a body.

HTTP Head

The HEAD request is similar to a GET request. Instead of returning the resource, it only returns the headers associated with the resource. A response to the HEAD request doesn't return a body. To make an HTTP HEAD request, given an HttpClient and a URI, use the HttpClient.SendAsync method with the HttpMethod set to HttpMethod.Head:

static async Task HeadAsync(HttpClient client)
{
    using HttpRequestMessage request = new(
        HttpMethod.Head, 
        "https://www.example.com");

    using HttpResponseMessage response = await client.SendAsync(request);

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    foreach (var header in response.Headers)
    {
        WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
    }
    WriteLine();

    // Expected output:
    //   HEAD https://www.example.com/ HTTP/1.1
    //   Accept-Ranges: bytes
    //   Age: 550374
    //   Cache-Control: max-age=604800
    //   Date: Wed, 10 Aug 2022 17:24:55 GMT
    //   ETag: "3147526947"
    //   Server: ECS, (cha / 80E2)
    //   X-Cache: HIT
}

The preceding code:

  • Makes a HEAD request to "https://www.example.com/".
  • Ensures that the response is successful, and writes the request details to the console.
  • Iterates over all of the response headers, writing each one to the console.

HTTP Options

The OPTIONS request is used to identify which HTTP methods a server or endpoint supports. To make an HTTP OPTIONS request, given an HttpClient and a URI, use the HttpClient.SendAsync method with the HttpMethod set to HttpMethod.Options:

static async Task OptionsAsync(HttpClient client)
{
    using HttpRequestMessage request = new(
        HttpMethod.Options, 
        "https://www.example.com");

    using HttpResponseMessage response = await client.SendAsync(request);

    response.EnsureSuccessStatusCode()
        .WriteRequestToConsole();

    foreach (var header in response.Content.Headers)
    {
        WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
    }
    WriteLine();

    // Expected output
    //   OPTIONS https://www.example.com/ HTTP/1.1
    //   Allow: OPTIONS, GET, HEAD, POST
    //   Content-Type: text/html; charset=utf-8
    //   Expires: Wed, 17 Aug 2022 17:28:42 GMT
    //   Content-Length: 0
}

The preceding code:

  • Sends an OPTIONS HTTP request to "https://www.example.com/".
  • Ensures that the response is successful, and writes the request details to the console.
  • Iterates over all of the response content headers, writing each one to the console.

HTTP Trace

The TRACE request can be useful for debugging as it provides application-level loop-back of the request message. To make an HTTP TRACE request, create an HttpRequestMessage using the HttpMethod.Trace:

using HttpRequestMessage request = new(
    HttpMethod.Trace, 
    "{ValidRequestUri}");

Caution

The TRACE HTTP verb is not supported by all HTTP servers. It can expose a security vulnerability if used unwisely. For more information, see Open Web Application Security Project (OWASP): Cross Site Tracing.

Handle an HTTP response

Whenever you're handling an HTTP response, you interact with the HttpResponseMessage type. Several members are used when evaluating the validity of a response. The HTTP status code is available via the HttpResponseMessage.StatusCode property. Imagine that you've sent a request given a client instance:

using HttpResponseMessage response = await client.SendAsync(request);

To ensure that the response is OK (HTTP status code 200), you can evaluate it as shown in the following example:

if (response is { StatusCode: HttpStatusCode.OK })
{
    // Omitted for brevity...
}

There are additional HTTP status codes that represent a successful response, such as CREATED (HTTP status code 201), ACCEPTED (HTTP status code 202), NO CONTENT (HTTP status code 204), and RESET CONTENT (HTTP status code 205). You can use the HttpResponseMessage.IsSuccessStatusCode property to evaluate these codes as well, which ensures that the response status code is within the range 200-299:

if (response.IsSuccessStatusCode)
{
    // Omitted for brevity...
}

If you need to have the framework throw the HttpRequestException, you can call the HttpResponseMessage.EnsureSuccessStatusCode() method:

response.EnsureSuccessStatusCode();

This code will throw an HttpRequestException if the response status code is not within the 200-299 range.

HTTP response errors

The HTTP response object (HttpResponseMessage), when not successful, contains information about the error. The HttpWebResponse.StatusCode property can be used to evaluate the error code.

For more information, see Client error status codes and Server error status codes.

HTTP valid content responses

With a valid response, you can access the response body using the Content property. The body is available as an HttpContent instance, which you can use to access the body as a stream, byte array, or string:

await using Stream responseStream =
    await response.Content.ReadAsStreamAsync();

In the preceding code, the responseStream can be used to read the response body.

byte[] responseByteArray = await response.Content.ReadAsByteArrayAsync();

In the preceding code, the responseByteArray can be used to read the response body.

string responseString = await response.Content.ReadAsStringAsync();

In the preceding code, the responseString can be used to read the response body.

Finally, when you know an HTTP endpoint returns JSON, you can deserialize the response body into any valid C# object by using the System.Net.Http.Json NuGet package:

T? result = await response.Content.ReadFromJsonAsync<T>();

In the preceding code, result is the response body deserialized as the type T.

HTTP proxy

An HTTP proxy can be configured in one of two ways. A default is specified on the HttpClient.DefaultProxy property. Alternatively, you can specify a proxy on the HttpClientHandler.Proxy property.

Global default proxy

The HttpClient.DefaultProxy is a static property that determines the default proxy that all HttpClient instances use if no proxy is set explicitly in the HttpClientHandler passed through its constructor.

The default instance returned by this property will initialize following a different set of rules depending on your platform:

  • For Windows: Reads proxy configuration from environment variables or, if those are not defined, from the user's proxy settings.
  • For macOS: Reads proxy configuration from environment variables or, if those are not defined, from the system's proxy settings.
  • For Linux: Reads proxy configuration from environment variables or, in case those are not defined, this property initializes a non-configured instance that bypasses all addresses.

The environment variables used for DefaultProxy initialization on Windows and Unix-based platforms are:

  • HTTP_PROXY: the proxy server used on HTTP requests.
  • HTTPS_PROXY: the proxy server used on HTTPS requests.
  • ALL_PROXY: the proxy server used on HTTP and/or HTTPS requests in case HTTP_PROXY and/or HTTPS_PROXY are not defined.
  • NO_PROXY: a comma-separated list of hostnames that should be excluded from proxying. Asterisks are not supported for wildcards; use a leading dot in case you want to match a subdomain. Examples: NO_PROXY=.example.com (with leading dot) will match www.example.com, but will not match example.com. NO_PROXY=example.com (without leading dot) will not match www.example.com. This behavior might be revisited in the future to match other ecosystems better.

On systems where environment variables are case-sensitive, the variable names may be all lowercase or all uppercase. The lowercase names are checked first.

The proxy server may be a hostname or IP address, optionally followed by a colon and port number, or it may be an http URL, optionally including a username and password for proxy authentication. The URL must be start with http, not https, and cannot include any text after the hostname, IP, or port.

Proxy per client

The HttpClientHandler.Proxy property identifies the WebProxy object to use to process requests to Internet resources. To specify that no proxy should be used, set the Proxy property to the proxy instance returned by the GlobalProxySelection.GetEmptyWebProxy() method.

The local computer or application config file may specify that a default proxy be used. If the Proxy property is specified, then the proxy settings from the Proxy property override the local computer or application config file and the handler will use the proxy settings specified. If no proxy is specified in a config file and the Proxy property is unspecified, the handler uses the proxy settings inherited from the local computer. If there are no proxy settings, the request is sent directly to the server.

The HttpClientHandler class parses a proxy bypass list with wildcard characters inherited from local computer settings. For example, the HttpClientHandler class will parse a bypass list of "nt*" from browsers as a regular expression of "nt.*". So a URL of http://nt.com would bypass the proxy using the HttpClientHandler class.

The HttpClientHandler class supports local proxy bypass. The class considers a destination to be local if any of the following conditions are met:

  1. The destination contains a flat name (no dots in the URL).
  2. The destination contains a loopback address (Loopback or IPv6Loopback) or the destination contains an IPAddress assigned to the local computer.
  3. The domain suffix of the destination matches the local computer's domain suffix (DomainName).

For more information about configuring a proxy, see:

See also