Rapid web requests to many different websites using HttpClient C# - c#

My team maintains a tool that is responsible for doing rapid verification of over 1000 different client websites. The tool is a Windows Service (.NET 4.5.2, C#) that reads requests from a queue, and executes a 'health check' for each request. It usually handles over 500 requests a minute, but can be responsible for more. Each request takes a second or two to execute.
A request contains a Uri and credentials needed for doing the health check. A health check is a POST against the AUTH page with the credentials (the app has custom auth, it's not header based auth), and then a GET to the home page, with a quick verification that it's the home page we expect. It then goes to a status page in the application, and does some quick checks against that. The GET requests have to use the cookies from the Set-Cookie header in the auth post.
We've been having performance problems with the tool as it scales. It currently creates a new HttpWebRequest object for each post and get in the process. There is a shared CookieContainer that is populated by the first post, so that we can get to the home page and then the status page.
I want to change this service to use the HttpClient object available in .NET 4.5. The problem is everywhere I read online says you want to avoid rapid creation and destruction of HttpClients. You'd rather keep one instance alive for the lifetime of the application. The problem I have is that HttpClient seems to work really well against one endpoint, not many.
I have looked into several options, and am not sure which is best to proceed:
Create a new HttpClient for each request, and use it for the duration of that request. That means it will live for a couple seconds, and be used for 3 calls. This would not be easy to implement, but I'm concerned about the overhead of creating and destroying hundreds of HttpClients a minute.
Figure out if it's possible to use one HttpClient instance for different endpoints by avoiding usage of a BaseAddress, and using the client to pass HttpRequestMessages using SendAsync. I haven't been able to figure out cookies with this method yet. To avoid having the HttpClient store the cookies, I set UseCookies to false in the HttpClientHandler, and tried managing cookies via headers in the HttpRequest/ResponseMessages themselves, but it looks like HttpClient simply strips cookies when UseCookies is set to false, so I was unable to pass cookies between request. edit: cookies work fine because they are stored per domain.
Store several hundred different HttpClient instances in some sort of dictionary, and pull the appropriate one for each Uri as the requests come in. I'm not sure about the memory overhead on this though. Also each unique Uri is only verified once every 5 minutes, so I'm not sure if having an HttpClient used once every 5 minutes keeps an unnecessary number of ports open.
Keep using HttpWebRequests. Maybe this older method still performs better in this situation.
If anyone has faced a similar issue, I'd love some input on where to proceed on this.
Thanks!

The problem with creating new HttpClients for each request is that HttpClientHandler will close the underlying TCP/IP connection. However, if you are using each HttpClient for the 3 requests to one host and then hitting a different host, then keeping the connection open doesn't help when you move to a new host. So, you probably will not see perf problem with one client per host. HttpClient itself is a very lightweight object. It isn't going to cost much to create one.
However, HttpClient simply delegates the real work to HttpClientHandler which uses HttpWebRequest under the covers, therefore will be unlikely to have any better performance than directly using HttpWebRequest.
If you are looking for better performance, then I suggest looking into replacing HttpClientHandler with the new WinHttpHandler which bypasses HttpWebRequest and goes directly to the Win32 API to make calls.
The full source is available for WinHttpHandler on GitHub so you can see exactly how it handles cookies and credentials.
And I would be really interested to hear if you do get much better perf with WinHttpHandler.

To start with, what part of this would you need modified to suit your needs?
var urisToCheck = new List<Uri>(); //get these somehow
//basic auth work?
var credentials = new NetworkCredential("user", "pass");
var handler = new HttpClientHandler { Credentials = credentials };
var client = new HttpClient(handler);
Parallel.ForEach(urisToCheck,
async uri =>
{
var response = await client.GetAsync(uri.AbsoluteUri);
//check for whatever you want here
}
);

here's my basic API client that uses the same HttpClient object for every request.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class MyApiClient : IDisposable
{
private readonly TimeSpan _timeout;
private HttpClient _httpClient;
private HttpClientHandler _httpClientHandler;
private readonly string _baseUrl;
private const string ClientUserAgent = "my-api-client-v1";
private const string MediaTypeJson = "application/json";
public MyApiClient(string baseUrl, TimeSpan? timeout = null)
{
_baseUrl = NormalizeBaseUrl(baseUrl);
_timeout = timeout ?? TimeSpan.FromSeconds(90);
}
public async Task<string> PostAsync(string url, object input)
{
EnsureHttpClientCreated();
using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
{
using (var response = await _httpClient.PostAsync(url, requestContent))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
}
public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
{
var strResponse = await PostAsync(url, input);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
{
var strResponse = await GetAsync(url);
return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
public async Task<string> GetAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.GetAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> PutAsync(string url, object input)
{
return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
}
public async Task<string> PutAsync(string url, HttpContent content)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.PutAsync(url, content))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public async Task<string> DeleteAsync(string url)
{
EnsureHttpClientCreated();
using (var response = await _httpClient.DeleteAsync(url))
{
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
public void Dispose()
{
_httpClientHandler?.Dispose();
_httpClient?.Dispose();
}
private void CreateHttpClient()
{
_httpClientHandler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
};
_httpClient = new HttpClient(_httpClientHandler, false)
{
Timeout = _timeout
};
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);
if (!string.IsNullOrWhiteSpace(_baseUrl))
{
_httpClient.BaseAddress = new Uri(_baseUrl);
}
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
}
private void EnsureHttpClientCreated()
{
if (_httpClient == null)
{
CreateHttpClient();
}
}
private static string ConvertToJsonString(object obj)
{
if (obj == null)
{
return string.Empty;
}
return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
private static string NormalizeBaseUrl(string url)
{
return url.EndsWith("/") ? url : url + "/";
}
}
Usage
using ( var client = new MyApiClient("http://localhost:8080"))
{
var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Register this object as singleton to your dependency injection library. It's safe to reuse because it's stateless.
Do NOT recreate HTTPClient for each request.
Reuse Httpclient as much as possible

Related

HttpClient changing request URI when called from WebAPI

I am implementing a transparent server-side proxy for an ASP.NET MVC application which wants to communicate with an API on another server; the code is fairly straightforward:
public class TransparentProxyDelegatingHandler : DelegatingHandler
{
private static readonly Uri ApiUri;
private static readonly HttpClient Client;
static TransparentProxyDelegatingHandler()
{
var apiServer = new Uri(ConfigurationManager.AppSettings["ApiUrl"]);
ApiUri = new Uri(apiServer);
Client = new HttpClient();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Add("X-Forwarded-For", request.GetClientIpAddress());
request.RequestUri = TranslateIncomingRequestToUpstreamApi(request);
request.Headers.AcceptEncoding.Clear();
var response = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
return response;
}
private static Uri TranslateIncomingRequestToUpstreamApi(HttpRequestMessage request)
{
var forwardUri = new UriBuilder(request.RequestUri)
{
Host = ApiUri.Host,
Path = request.RequestUri.AbsolutePath.Replace("/Proxy", string.Empty)
};
return forwardUri.Uri;
}
}
So if I query GET https://ui.myserver.com/proxy/timesheets?from=2018-01-01, the request URI gets changed by the proxy to GET https://api.myserver.com/timesheets?from=2018-01-01, and I can verify this in the debugger; however, when the SendAsync method is invoked, the hostname part of the request URI is changed back to https://ui.myserver.com, and the call fails.
Why is it changing the value of request.RequestUri when I call SendAsync ? I've checked the source in GitHub (https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs), but none of the conditions for changing the value seem to apply in my case. Unfortunately the GitHub source does not align with the debug symbols, so I can't seem to step into the HttpClient source to figure out what's really going on.
OK, I found the cause of my problem; I needed to set change the Host header; the initial request to the proxy set it to the hostname of the UI (ui.myserver.com), and that overrides the hostname of the proxy that was set in the request. So if I add the following:
request.Headers.Host = $"{ApiUri.Host}:{ApiUri.Port}";
then everything magically works.

ASP .NET Core 2.1-preview2 HttpClient deadlock

I have ASP.NET Core 2.1 application hosted on Azure web app. I am sending photos base64 string over WebSockets and then by HttpClient to Azure Face API.
After some 150-250 requests HttpClient stops responding and I can't use HttpClient class in any part of my application.
In my localhost it works properly and I never get this problem.
public class FaceApiHttpClient
{
private HttpClient _client;
public FaceApiHttpClient(HttpClient client)
{
_client = client;
}
public async Task<string> GetStringAsync(byte[] byteData,string uri)
{
using (ByteArrayContent content = new ByteArrayContent(byteData))
{
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
HttpResponseMessage response = await _client.PostAsync(uri, content).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
}
DI:
services.AddHttpClient<FaceApiHttpClient>(
client => {
client.BaseAddress = new Uri("xxx");
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "xxx");
});
The method from FaceApiClient is invoke in a Scoped Service:
public interface IFaceAPIService
{
Task<DataServiceResult<List<Face>>> GetFacesDataFromImage(byte[] byteArray);
}
public class FaceAPIService: ServiceBase, IFaceAPIService
{
private readonly IServerLogger _serverLogger;
private FaceApiHttpClient _httpClient;
//Consts
public const string _APIKey = "xxx";
public const string _BaseURL = "xxx";
public FaceAPIService(IServerLogger serverLogger, FaceApiHttpClient client)
{
_serverLogger = serverLogger;
_httpClient = client;
}
public async Task<DataServiceResult<List<Face>>> GetFacesDataFromImage(byte[] byteData)
{
try
{
// Request parameters. A third optional parameter is "details".
string requestParameters = "returnFaceId=true&returnFaceLandmarks=false&returnFaceAttributes=age,gender,headPose,smile,facialHair,glasses,emotion,hair,makeup,occlusion,accessories,blur,exposure,noise";
// Assemble the URI for the REST API Call.
string uri = _BaseURL + "/detect" + "?" + requestParameters;
var result = await _httpClient.GetStringAsync(byteData, uri).ConfigureAwait(false);
List<Face> faces = JsonConvert.DeserializeObject<List<Face>>(result);
return Success(faces);
}
catch (Exception ex)
{
_serverLogger.LogExceptionFromService(ex);
return DataServiceResult.ErrorResult<List<Face>>(ex.Message);
}
}
}
a) on localhost enviroment it works. I run 11 simulators with many request per seconds and it never broke (10 hours of simulators, over 20k requests).
b) HttpClient stops working in any part of application not only in one class.
How to fix this?
Consider changing up the design a bit.
Using a typed client the assumption is that its configuration is something that will not change frequently and that it should be added once when registering the typed client.
services.AddHttpClient<FaceApiHttpClient>(_ => {
_.BaseAddress = new Uri(Configuration["OcpApimBaseURL"]);
var apiKey = Configuration["OcpApimSubscriptionKey"];
_.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", apiKey);
_.Timeout = new TimeSpan(0, 0, 10);
});
Which would allow the typed client to no have to add the key for every call
public class FaceApiHttpClient {
private readonly HttpClient client;
public FaceApiHttpClient(HttpClient client) {
this.client = client;
}
public async Task<string> GetStringAsync(byte[] byteData, string uri) {
using (var content = new ByteArrayContent(byteData)) {
// This example uses content type "application/octet-stream".
// The other content types you can use are "application/json" and "multipart/form-data".
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
// Execute the REST API call.
HttpResponseMessage response; response = await _client.PostAsync(uri, content).ConfigureAwait(false);
// Get the JSON response.
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
}
}
It should be noted from ASP.NET Core 2.1-preview1: Introducing HTTPClient factory
A typed client is, effectively, a transient service, meaning that a new instance is created each time one is needed and it will receive a new HttpClient instance each time it is constructed. This means that the configuration func, in this case retrieving the URI from configuration, will run every time something needs a FaceApiHttpClient.
Based on previous documentations, having that many clients created can pose problems, but the assumption here is that the developers of this new feature took that into consideration when designing it.
I say this because the issues you described are similar to previous problem with the same cause.
After release version of ASP .NET CORE 2.1 RC1 the problem is fixed. I updated project to new version and now there is no problem with deadlock.
The problem with deadlock was only in ASP .NET CORE 2.1 Preview 2 version.

Create a safe-threaded client library to connect to an external API

Sorry for the name of my post but I am going to try to explain my problem.
I am building a client library to request an external API and I try to figure out how to make the methods of my library safe-threaded.
Basically the main class of my library looks like this :
public class MyCompanyApiClient
{
private readonly HttpClient _httpClient;
public MyCompanyApiClient(string baseUrl)
{
_httpClient = new HttpClient() {BaseAddress = new Uri(baseUrl)};
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<MyClassB> PostData(int id, MyClassA dataToPost)
{
var request = new HttpRequestMessage(HttpMethod.Post, $"objects/{id}");
request.Content = new StringContent(JsonConvert.SerializeObject(dataToPost), Encoding.UTF8,
"application/json");
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var stringContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<MyClassB>(stringContent);
}
public async Task<MyClassA > GetById(int id)
{
...
}
}
I'd like to ensure that any instance members of my client class are safe-threaded.
For the moment I plan to wrap the code inside each method into a Task.Run:
public async Task<MyClassB> PostData(int id, MyClassA dataToPost)
{
return await Task.Run(async () =>
{
var request = new HttpRequestMessage(HttpMethod.Post, $"objects/{id}");
request.Content = new StringContent(JsonConvert.SerializeObject(dataToPost), Encoding.UTF8,
"application/json");
var response = await _httpClient.SendAsync(request);
var stringContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<MyClassB>(stringContent);
});
}
By the way I don't even know if it is going to ensure this method to be safe-threaded.
Any help aprreciated ^^
Thread-safe doesn't mean multi-threading.
It means that some code is aware that it'll be used in multi-threaded cases, and they won't get corrupted, won't produce deadlocks and other threading-related issues.
Sometimes there's code that it's thread-safe per se, while sometimes you need to use thread-synchronization approaches like monitors, mutexes, reset events, semaphores and others, to protect critical code sections from being executed by one, two or n threads at once in order to avoid, again, deadlocks, corruptions and, basically, unexpected behaviors.
In your case, it seems like you consider thread-safe launching your PostDataas a thread in the thread pool. This is a bad choice, you don't need a new thread for this.

Adding headers when using httpClient.GetAsync

I'm implementing an API made by other colleagues with Apiary.io, in a Windows Store app project.
They show this example of a method I have to implement:
var baseAddress = new Uri("https://private-a8014-xxxxxx.apiary-mock.com/");
using (var httpClient = new HttpClient{ BaseAddress = baseAddress })
{
using (var response = await httpClient.GetAsync("user/list{?organizationId}"))
{
string responseData = await response.Content.ReadAsStringAsync();
}
}
In this and some other methods, I need to have a header with a token that I get before.
Here's an image of Postman (chrome extension) with the header I'm talking about:
How do I add that Authorization header to the request?
A later answer, but because no one gave this solution...
If you do not want to set the header on the HttpClient instance by adding it to the DefaultRequestHeaders, you could set headers per request.
But you will be obliged to use the SendAsync() method.
This is the right solution if you want to reuse the HttpClient -- which is a good practice for
performance and port exhaustion problems
doing something thread-safe
not sending the same headers every time
Use it like this:
using (var requestMessage =
new HttpRequestMessage(HttpMethod.Get, "https://your.site.com"))
{
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", your_token);
await httpClient.SendAsync(requestMessage);
}
When using GetAsync with the HttpClient you can add the authorization headers like so:
httpClient.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", "Your Oauth token");
This does add the authorization header for the lifetime of the HttpClient so is useful if you are hitting one site where the authorization header doesn't change.
Here is an detailed SO answer
The accepted answer works but can get complicated when you want to try adding Accept headers. This is what I ended up with. It seems simpler to me, so I think I'll stick with it in the future:
client.DefaultRequestHeaders.Add("Accept", "application/*+xml;version=5.1");
client.DefaultRequestHeaders.Add("Authorization", "Basic " + authstring);
Sometimes, you only need this code.
httpClient.DefaultRequestHeaders.Add("token", token);
Following the greenhoorn's answer, you can use "Extensions" like this:
public static class HttpClientExtensions
{
public static HttpClient AddTokenToHeader(this HttpClient cl, string token)
{
//int timeoutSec = 90;
//cl.Timeout = new TimeSpan(0, 0, timeoutSec);
string contentType = "application/json";
cl.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(contentType));
cl.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", token));
var userAgent = "d-fens HttpClient";
cl.DefaultRequestHeaders.Add("User-Agent", userAgent);
return cl;
}
}
And use:
string _tokenUpdated = "TOKEN";
HttpClient _client;
_client.AddTokenToHeader(_tokenUpdated).GetAsync("/api/values")
These days, if you are using MS Dependency Injection, it's highly recomended to plug in the IHttpClientFactory:
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
var httpClient = _httpClientFactory.CreateClient("GitHub");
This way you avoid adding default request headers to a globally shared httpclient and moreover don't have to deal with manual creation of the HttpRequestMessage.
Source:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0#consumption-patterns

How can I tell if HttpClient is connecting via HTTPS

I am using HttpClient to talk to an API. The server will automatically redirect http:// requests to https:// if enabled. So, in order to protect the users API key I want to create a test connection to the website to see if I am redirected before sending over the API Key.
The HttpClient redirects properly, but I cannot seem to find a decent way to find out if the client is using HTTPS or not. I know I could test whether https:// exists within response.RequestMessage.RequestUri but this seems a little flaky
public void DetectTransportMethod()
{
using (HttpClient client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(20);
using (HttpResponseMessage response = client.GetAsync(this.HttpHostname).Result)
{
using (HttpContent content = response.Content)
{
// check if the method is HTTPS or not
}
}
}
}
The documentation of HttpClient (MSDN) and HttpResponseMessage (MSDN) do not contain any methods that can be used to determine whether a request has been made over https. Even though checking the URI of the HttpResponseMessage does indeed sound flaky I'm afraid it's the easiest and most readable option. Implementing this as a extension method for HttpResponseMessage probably is the most readable. To ensure that the HttpClient you are using can be redirected make sure that the WebRequestHandler (MSDN) passed into the HttpClient has the AllowAutoRedirect (MSDN) property set to true.
See the following extension method:
static class Extensions
{
public static bool IsSecure(this HttpResponseMessage message)
{
rreturn message.RequestMessage.RequestUri.Scheme == "https";
}
}
And the following Console application that demonstrates it works. For this to work however, the HTTP server has to upgrade the connection to https (Like Facebook does) and the AllowAutoRedirect property of the WebRequestHandler has to be set to true.
static void Main(string[] args)
{
using (var client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = true}))
{
client.Timeout = TimeSpan.FromSeconds(20);
using (var response = client.GetAsync("http://www.geenstijl.nl").Result)
{
Console.WriteLine(response.IsSecure());
}
using (var response = client.GetAsync("http://www.facebook.com").Result)
{
Console.WriteLine(response.IsSecure());
}
}
Console.ReadLine();
}

Categories

Resources