Updated on
HttpClient sends no User-Agent header unless we add one. We can set it on a single HttpRequestMessage, or set it once on the client’s DefaultRequestHeaders so that every request it sends carries the same value.
We’ll look at what the header holds first, then set it both ways on .NET 10, and finally read it on the server.
Let’s start.
What Is a User-Agent Header?
Before we learn to set User-Agent on HttpClient, let’s understand the basics.
The user agent is a set of strings sent by web applications or API clients while making HTTP calls to the backend webservers. We generally specify it in the format:
User-Agent: Product/ProductVersion <comments>
Let’s understand this.
The header value contains three distinct pieces of information:
- Product: Information about the product making the HTTP calls
- Product Version: The version of the product
- Comments: Zero or more comments that provide more detail about the product
For example, let’s see how the header looks for version 1.1 of a product called CodeMazeDesktopApp:
User-Agent: CodeMazeDesktopApp/1.1
Moreover, we can use User-Agent values:
- To check and limit access to our APIs to only certain products or browsers
- To check and tailor the user experience of our web applications based on the type of device making the HTTP call
- To analyze the user engagement behaviors based on the device used to access our web applications or APIs
The first of those uses needs care, because the client writes this value and any client can claim to be any product. That makes the header good for analytics and for tailoring a response, but never an access control on its own.
With this basic knowledge, let’s explore how we can set the User-Agent while making HTTP calls.
Does HttpClient Send a User-Agent by Default?
No. A new HttpClient() sends no User-Agent header at all. When we captured the raw request it sent on .NET 10, it carried the request line and a Host header, and nothing else.
A client from IHttpClientFactory behaves the same way until we configure one. The header only appears when our own code adds it, either on a single request or as a default on the client.
That surprises people, because browsers and curl send a User-Agent on every request. A missing one is unusual enough that some servers refuse the request. GitHub is the best-known example. Its REST API documentation says “Requests with no User-Agent header will be rejected.”
We checked that from C#. In October 2026, a bare HttpClient calling https://api.github.com/zen got 403 Forbidden. The same call with CodeMazeDesktopApp/1.1 as the user agent got 200 OK.
So when a request works in curl and fails from our C# code, a missing User-Agent is one of the first things to check.
How Do We Set the User-Agent on a Single Request?
Let’s look at a simple way to set the User-Agent header for individual HTTP requests that we make with HttpClient:
public class Program
{
public static void Main(string[] args)
{
var userAgentValue = SetUserAgentOnHttpRequestMessage("CodeMazeDesktopApp/1.1");
}
public static string SetUserAgentOnHttpRequestMessage(string userAgentValue)
{
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.Headers.Add("User-Agent", userAgentValue);
return httpRequestMessage.Headers.UserAgent.ToString();
}
}
First, in our SetUserAgentOnHttpRequestMessage() method, we initialize an instance of the HttpRequestMessage. Next, we use the Add() method of the Headers property to set the value of the user agent. Finally, we return the string value of the UserAgent property.
When we test this method from the Main() method of our console app, it will return us the result:
CodeMazeDesktopApp/1.1
Add() checks the value against the header’s grammar before it accepts it. A value like [email protected] throws a FormatException, because @ is not allowed in a product name, while TryAddWithoutValidation() stores any string as it is. Spaces separate products, so Code Maze Desktop comes through as three products rather than one name.
The typed UserAgent collection has the same parser. ParseAdd() takes a whole string such as a browser’s user agent, and TryParseAdd() returns false instead of throwing.
With this done, let’s see how we can set the header cleanly using the inbuilt capabilities provided by .NET.
How Do We Build the User-Agent With ProductInfoHeaderValue?
We can use the ProductHeaderValue and ProductInfoHeaderValue classes available in the System.Net.Http.Headers namespace to set the User-Agent header cleanly.
Accordingly, let’s refactor our code:
using System.Net.Http.Headers;
public class Program
{
public static void Main(string[] args)
{
var userAgentValue = SetUserAgentOnHttpRequestMessage("CodeMazeDesktopApp", "1.1");
}
public static string SetUserAgentOnHttpRequestMessage(string productName, string productVersion)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "/users/1234");
var productHeaderValue = new ProductHeaderValue(name: productName, version: productVersion);
var productInfoHeaderValue = new ProductInfoHeaderValue(product: productHeaderValue);
httpRequestMessage.Headers.UserAgent.Add(productInfoHeaderValue);
return httpRequestMessage.Headers.UserAgent.ToString();
}
}
First, we initialize an instance of ProductHeaderValue by providing the product name and version to its constructor. Next, we initialize an instance of the ProductInfoHeaderValue class. Then, we add the ProductInfoHeaderValue instance to the UserAgent collection on the Headers property using the Add() method. Finally, we return the string value of the UserAgent property.
When we test this method from the Main() method of our console app, it will return us the result:
CodeMazeDesktopApp/1.1
We have made our code cleaner now. Both classes check their input too. new ProductHeaderValue("Code Maze", "1.1") throws a FormatException because of the space. ProductInfoHeaderValue also has a constructor that takes a comment, so new ProductInfoHeaderValue("(+https://code-maze.com)") adds the <comments> part of the format we saw earlier. However, in this approach, our code sets the user agent value every time the HttpRequestMessage instance is created.
Let’s now see how to set a default User-Agent on an HttpClient.
How Do We Set the User-Agent Once for Every Request?
We set it once, on the client’s DefaultRequestHeaders.UserAgent collection, and every request that client sends carries it. Microsoft Learn describes DefaultRequestHeaders as “the headers which should be sent with each request.”
With IHttpClientFactory, the natural place for that is the AddHttpClient() registration. The factory runs our configuration delegate on every client it creates, before the client sends anything, so the header is there from the first call.
Setting it at registration also respects a rule on that same Learn page: we should not modify DefaultRequestHeaders while requests are still in flight.
A request can still override the default. When an HttpRequestMessage carries its own User-Agent, that value goes out instead of the client’s, not next to it. We confirmed that on the wire.
And when every client in the application needs the same value, ConfigureHttpClientDefaults(), added in .NET 8, applies one configuration to all of them, named or not.
As a best practice, we should use the HttpClientFactory to create HttpClient instances in an ASP.NET Core application. An ASP.NET Core app already has it, because Microsoft.Extensions.Http ships with the shared framework. Our example is a console app, so we install it first, along with Microsoft.Extensions.Hosting for the host:
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Http
With the packages now installed, let’s add the IHttpClientFactory service to our service collection:
public static IHost BuildHost()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddHttpClient("CodeMazeHttpClient", client =>
{
var productHeaderValue = new ProductHeaderValue(name: "CodeMazeDesktopApp", version: "1.1");
var productInfoHeaderValue = new ProductInfoHeaderValue(productHeaderValue);
client.DefaultRequestHeaders.UserAgent.Add(productInfoHeaderValue);
});
return builder.Build();
}
This method, like the ones that follow, goes inside our Program class, and Program.cs now needs using Microsoft.Extensions.DependencyInjection; and using Microsoft.Extensions.Hosting; at the top. We begin by creating a builder with Host.CreateApplicationBuilder(). This builder is the utility to build the container that stores all the services that our app needs, and its Services property is where we register them. We add the IHttpClientFactory and the related services to the service collection using the AddHttpClient() method. Here, we are adding a HttpClient named CodeMazeHttpClient. Next, we build the value with ProductHeaderValue and ProductInfoHeaderValue, as before. Furthermore, we add the default user agent using the Add() method on the UserAgent property on the DefaultRequestHeaders. Finally, we return the host by using the Build() command on the builder we just configured.
With the host now configured, let’s read the user agent of the HttpClient:
public static string GetDefaultAgentOnHttpClient(IHttpClientFactory httpClientFactory)
{
var httpClient = httpClientFactory.CreateClient("CodeMazeHttpClient");
return httpClient.DefaultRequestHeaders.UserAgent.ToString();
}
We create an instance of HttpClient named CodeMazeHttpClient using the CreateClient() method on the IHttpClientFactory. Finally, we return the string value of the UserAgent property.
Next, let’s invoke this method from the Main() method of our console app:
public static void Main(string[] args)
{
var host = BuildHost();
var httpClientFactory = host.Services.GetRequiredService<IHttpClientFactory>();
var defaultUserAgentOnHttpClient = GetDefaultAgentOnHttpClient(httpClientFactory);
}
We begin with building the host using the BuildHost() method we created earlier. Next, we request an instance of the IHttpClientFactory using the GetRequiredService<IHttpClientFactory>() method, which throws a clear exception if the service is missing instead of handing us null. Finally, we execute the GetDefaultAgentOnHttpClient() method and we get the result:
CodeMazeDesktopApp/1.1
Using this approach, we set the user agent only once in the HttpClient instance’s lifetime.
A single request can still send a different value. If we add a User-Agent to an HttpRequestMessage and send it through CodeMazeHttpClient, the server receives only the request’s value, CodeMazeReportJob/2.0. The same default-versus-per-request split applies to HttpClient timeouts.
When every client in the application should identify itself the same way, we don’t have to repeat the delegate for each registration. ConfigureHttpClientDefaults() applies one configuration to every client the factory creates:
public static IHost BuildHostWithUserAgentForAllClients()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.ConfigureHttpClientDefaults(httpClientBuilder =>
{
httpClientBuilder.ConfigureHttpClient(client =>
{
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("CodeMazeDesktopApp", "1.1"));
});
});
return builder.Build();
}
This time we use the shorter ProductInfoHeaderValue constructor, which takes the product name and version directly. A named client and the unnamed one from CreateClient() both report CodeMazeDesktopApp/1.1 now.
If a named registration also adds a User-Agent, it does not replace this one. The client ends up with both products, the default first, so we pick one place for the value.
In our example, we used a simple version of HostApplicationBuilder and IHttpClientFactory to create HttpClient. But, in real-world implementations, we should follow the best practices mentioned in our Using HttpClientFactory in ASP.NET Core Applications article.
DefaultRequestHeaders holds more than the user agent. The same collection is one way to add a bearer token to an HttpClient request. And when the value depends on the request itself, a delegating handler can set it on each outgoing request instead.
How Do We Read the User-Agent in ASP.NET Core?
On the receiving side, ASP.NET Core exposes the header as a typed property, Request.Headers.UserAgent. Let’s echo it back from a minimal API endpoint:
app.MapGet("/user-agent", (HttpRequest request) => request.Headers.UserAgent.ToString());
The property returns StringValues, so ToString() gives us the whole header value. When our CodeMazeHttpClient calls this endpoint, the response is CodeMazeDesktopApp/1.1. A bare HttpClient gets an empty string back, because there is no header to read.
In a controller, the same property is HttpContext.Request.Headers.UserAgent. Headers without a typed property are read by name, which we cover in our article on extracting a custom header in ASP.NET Core Web API.
Conclusion
To sum it up, in this article, we learned about the User-Agent header property and some of its uses. Next, we learned how to set the User-Agent header cleanly on the HttpRequestMessage object. Finally, we learned how to set a default User-Agent value which can be used for the entire lifetime of an HttpClient. We also saw that HttpClient sends no User-Agent until we add one, and how ASP.NET Core reads it on the other side.
Tested with .NET 10 (runtime 10.0.10, SDK 10.0.302), Microsoft.Extensions.Hosting and Microsoft.Extensions.Http 10.0.13, and xUnit 2.9.3.
