curl to HttpClient in C#: a side-by-side migration guide
Going from curl to HttpClient in C# usually means reading a curl ... line from API docs and rebuilding it by hand with HttpRequestMessage, StringContent and DefaultRequestHeaders. CurlDotNet gives you two shortcuts: generate the HttpClient code from the curl command, or skip the translation and run the curl command directly. This guide shows both, side by side with the hand-written HttpClient equivalent, and then covers moving existing HttpClient code to CurlDotNet.
Convert a curl command to C# HttpClient code
Curl.ToHttpClient parses a curl command and emits the equivalent HttpClient C#. Use it when you want plain HttpClient in your codebase but do not want to translate by hand.
using CurlDotNet;
var cmd = "curl -H 'Accept: application/json' -H 'User-Agent: MyApp/1.0' https://api.github.com/users/octocat";
Console.WriteLine(Curl.ToHttpClient(cmd));
Output:
// Generated by CurlDotNet from: curl -H 'Accept: application/json' -H 'User-Agent: MyApp/1.0' https://api.github.com/users/octocat
var handler = new HttpClientHandler
{
AllowAutoRedirect = false, // curl without -L returns the 3xx response itself
};
using var client = new HttpClient(handler);
client.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/json");
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "MyApp/1.0");
var response = await client.GetAsync("https://api.github.com/users/octocat");
var body = await response.Content.ReadAsStringAsync();
The generator covers method (with the real GetAsync/PostAsync/PutAsync/PatchAsync/DeleteAsync calls), URL, headers, -A, -b cookie strings, form, JSON and multipart (-F) bodies, -u credentials, -x proxies, -k, -L with --max-redirs (without -L the handler gets AllowAutoRedirect = false, because curl does not follow redirects unless asked), and -m as client.Timeout. Review the output before committing it; it is a starting point, not a full port of every curl option (retries, cookie jars and -w are not translated). The same parser also drives Curl.ToFetch, Curl.ToPythonRequests and Curl.ToPowershellCode.
Or run the curl command directly
If you do not need HttpClient specifically, the shortest path is to run the command as-is:
var result = await Curl.ExecuteAsync(cmd);
Console.WriteLine(result.StatusCode);
Console.WriteLine(result.Body);
Side by side: curl, HttpClient, CurlDotNet
GET with headers
curl -H "Accept: application/json" https://httpbin.org/headers
HttpClient:
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Accept", "application/json");
var response = await client.GetAsync("https://httpbin.org/headers");
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -H 'Accept: application/json' https://httpbin.org/headers");
var result = await CurlRequestBuilder.Get("https://httpbin.org/headers")
.WithHeader("Accept", "application/json")
.ExecuteAsync();
curl -X POST JSON in C#
curl -X POST https://httpbin.org/post -H "Content-Type: application/json" -d '{"name":"Ada"}'
HttpClient:
using var client = new HttpClient();
var json = JsonSerializer.Serialize(new { name = "Ada" });
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://httpbin.org/post", content);
var body = await response.Content.ReadAsStringAsync();
CurlDotNet:
var result = await Curl.ExecuteAsync(@"curl -X POST https://httpbin.org/post
-H 'Content-Type: application/json' -d '{""name"":""Ada""}'");
var result = await CurlRequestBuilder.Post("https://httpbin.org/post")
.WithJson(new { name = "Ada" })
.ExecuteAsync();
curl bearer token in C#
curl -H "Authorization: Bearer token123" https://httpbin.org/bearer
HttpClient:
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token123");
var response = await client.GetAsync("https://httpbin.org/bearer");
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -H 'Authorization: Bearer token123' https://httpbin.org/bearer");
var result = await CurlRequestBuilder.Get("https://httpbin.org/bearer").WithBearerToken("token123").ExecuteAsync();
curl basic auth in C#
curl -u user:passwd https://httpbin.org/basic-auth/user/passwd
HttpClient:
var bytes = Encoding.ASCII.GetBytes("user:passwd");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", Convert.ToBase64String(bytes));
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -u user:passwd https://httpbin.org/basic-auth/user/passwd");
curl timeout in C#
curl -m 10 https://httpbin.org/delay/2
HttpClient:
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
try { var response = await client.GetAsync("https://httpbin.org/delay/2"); }
catch (TaskCanceledException) { Console.WriteLine("Timed out"); }
CurlDotNet:
using CurlDotNet.Exceptions;
try { var result = await Curl.ExecuteAsync("curl -m 10 https://httpbin.org/delay/2"); }
catch (CurlTimeoutException) { Console.WriteLine("Timed out"); }
The defaults differ: HttpClient gives up after 100 seconds unless you change Timeout, while CurlDotNet, like curl, has no timeout unless you pass -m (or set Curl.DefaultMaxTimeSeconds). -m bounds the whole transfer including the body, and an expired -m throws CurlOperationTimeoutException, never TaskCanceledException; a cancelled CancellationToken throws OperationCanceledException, as with HttpClient.
curl follow redirects in C#
HttpClient follows redirects by default; curl does not unless -L is given. CurlDotNet follows curl.
var result = await Curl.ExecuteAsync("curl -L https://httpbin.org/redirect/2"); // 200
var raw = await Curl.ExecuteAsync("curl https://httpbin.org/redirect/2"); // 302
To get HttpClient-like behaviour everywhere, set Curl.DefaultFollowRedirects = true once at startup.
curl ignore SSL certificate in C#
HttpClient:
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
using var client = new HttpClient(handler);
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -k https://self-signed.badssl.com/");
curl download file in C#
HttpClient:
using var stream = await client.GetStreamAsync("https://httpbin.org/image/png");
using var file = File.Create("image.png");
await stream.CopyToAsync(file);
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -o image.png https://httpbin.org/image/png");
// or: await Curl.DownloadAsync("https://httpbin.org/image/png", "image.png");
Form post and file upload
HttpClient:
using var form = new MultipartFormDataContent();
form.Add(new StringContent("Report"), "title");
form.Add(new StreamContent(File.OpenRead("report.pdf")), "file", "report.pdf");
var response = await client.PostAsync("https://httpbin.org/post", form);
CurlDotNet:
var result = await Curl.ExecuteAsync("curl -F 'title=Report' -F 'file=@report.pdf' https://httpbin.org/post");
curl proxy in C#
HttpClient:
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.internal:8080") { Credentials = new NetworkCredential("alice", "secret") }
};
using var client = new HttpClient(handler);
CurlDotNet:
var result = await Curl.ExecuteAsync(
"curl -x http://proxy.internal:8080 --proxy-user alice:secret https://httpbin.org/ip");
Error handling compared
HttpClient throws HttpRequestException for network failures and, after EnsureSuccessStatusCode(), for HTTP errors; timeouts surface as TaskCanceledException. CurlDotNet returns HTTP errors as a normal CurlResult with IsSuccess == false (or throws CurlHttpReturnedErrorException with -f), and throws typed CurlException subclasses carrying curl’s exit code for network failures and -m timeouts. Cancellation is the standard OperationCanceledException in both.
using CurlDotNet.Exceptions;
try
{
var result = await Curl.ExecuteAsync("curl -m 10 https://httpbin.org/status/404");
if (!result.IsSuccess)
Console.WriteLine($"HTTP {result.StatusCode}");
}
catch (CurlTimeoutException) { /* -m exceeded */ }
catch (CurlConnectionException ex) { /* DNS, refused, reset */ }
catch (CurlSslException ex) { /* certificate problems */ }
Call result.EnsureSuccess() if you prefer an exception (CurlDotNet.Core.CurlHttpException) for non-2xx responses, as with EnsureSuccessStatusCode().
Dependency injection
HttpClient:
services.AddHttpClient<GitHubService>(client =>
{
client.BaseAddress = new Uri("https://api.github.com");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
});
CurlDotNet with LibCurl (reusable, IDisposable, register as a singleton):
using CurlDotNet.Lib;
services.AddSingleton(_ => new LibCurl()
.WithUserAgent("MyApp/1.0")
.WithTimeout(TimeSpan.FromSeconds(20)));
public class GitHubService(LibCurl curl)
{
public Task<CurlResult> GetUserAsync(string login) =>
curl.GetAsync($"https://api.github.com/users/{login}");
}
Or CurlApiClient for a single base URL: new CurlApiClient("https://httpbin.org") with GetAsync("/get"), PostJsonAsync, PutJsonAsync, PatchJsonAsync and DeleteAsync.
Migration checklist: HttpClient to CurlDotNet
- Replace
client.GetAsync+ReadAsStringAsyncpairs withCurl.ExecuteAsync(...)andresult.Body. - Replace
StringContentJSON bodies with-d/WithJson/Curl.PostJsonAsync. - Replace
response.IsSuccessStatusCodewithresult.IsSuccess;(int)response.StatusCodewithresult.StatusCode. - Replace
EnsureSuccessStatusCode()withresult.EnsureSuccess(). - Replace
JsonSerializer.Deserialize<T>(body)withresult.ParseJson<T>(). Note thatParseJson<T>()matches property names case-insensitively, unlikeJsonSerializer’s defaults (ReadFromJsonAsyncuses web defaults, which are case-insensitive too). - Add
-L(or setCurl.DefaultFollowRedirects = true) where you relied onHttpClientfollowing redirects automatically. User-Agent:HttpClientsends none by default, while CurlDotNet sendsUser-Agent: curl/8.7.1andAccept: */*like curl, so APIs that require one (GitHub’s) work as-is. Use-AorCurl.DefaultUserAgentto name your app.- Timeouts:
HttpClient’s 100-second default does not exist in CurlDotNet; add-mwhere you relied on it. - Retries: replace Polly-style retry handlers for transient errors with
--retry NorCurlSettings.WithRetries(details). - Register
LibCurlorCurlApiClientas a singleton where you usedAddHttpClient.