CurlDotNet API guide: curl commands, fluent builder and LibCurl in C#
This CurlDotNet API guide covers the three ways to make requests in C#: the static Curl string API that runs curl commands, the CurlRequestBuilder fluent API, and the reusable LibCurl client. All three return the same CurlResult.
The three APIs
Curl string API
using CurlDotNet;
var result = await Curl.ExecuteAsync("curl -A 'MyApp/1.0' -H 'Accept: application/json' https://api.github.com/repos/curl/curl");
Best for copy-pasting commands from API docs, shell scripts and Stack Overflow. The leading curl is optional and multi-line commands with \ continuations are accepted.
CurlRequestBuilder fluent API
using CurlDotNet.Core;
var result = await CurlRequestBuilder.Get("https://api.github.com/repos/curl/curl")
.WithUserAgent("MyApp/1.0")
.WithHeader("Accept", "application/json")
.WithBearerToken("token123")
.ExecuteAsync();
Best for building requests programmatically with IntelliSense and compile-time checking. ToCurlCommand() gives you back the equivalent curl string for logging or bug reports.
LibCurl reusable client
using CurlDotNet.Lib;
using var curl = new LibCurl()
.WithUserAgent("MyApp/1.0")
.WithBearerToken("token123")
.WithTimeout(TimeSpan.FromSeconds(30));
var result = await curl.GetAsync("https://api.github.com/user");
Best for shared configuration and dependency injection. LibCurl is IDisposable; register one instance as a singleton.
Which API should you use?
| Need | Use |
|---|---|
| Run an existing curl command unchanged | String API |
| IntelliSense, refactoring, dynamic requests | CurlRequestBuilder |
| Shared headers, auth and timeouts across many calls, DI | LibCurl |
| Thin wrapper around one REST base URL | CurlApiClient (below) |
Curl static class
| Member | Purpose |
|---|---|
ExecuteAsync(string) / Execute(string) |
Run a curl command string |
ExecuteAsync(string, CancellationToken) |
Same, cancellable |
ExecuteAsync(string, CurlSettings) |
Same, with per-call settings (timeout, retries, redirects) |
ExecuteAsync(CurlOptions) / Execute(CurlOptions) |
Run from a pre-built options object (no command string, no quoting); ExecuteAsync(CurlOptions, CancellationToken) is cancellable |
GetAsync(url) / Get(url) |
Shorthand for curl <url> |
PostAsync(url, data) / Post(url, data) |
Shorthand for curl -d <data> <url> |
PostJsonAsync(url, object) / PostJson(url, object) |
Serialize an object and POST it as JSON |
DownloadAsync(url, path) / Download(url, path) |
Shorthand for curl -o <path> <url> |
ExecuteManyAsync(params string[]) |
Run several commands concurrently |
Validate(cmd) |
Parse a command without executing it |
ToHttpClient(cmd), ToFetch(cmd), ToPythonRequests(cmd), ToPowershellCode(cmd) |
Generate equivalent code in other stacks |
DefaultMaxTimeSeconds, DefaultConnectTimeoutSeconds, DefaultFollowRedirects, DefaultInsecure |
Process-wide defaults applied when a command does not specify them; out of the box 0 (no timeout), 0, false (no redirects without -L) and false, matching curl |
DefaultUserAgent |
User-Agent sent when a command sets none; defaults to curl/8.7.1 |
CurlCompatVersion |
The curl release whose behaviour is reproduced ("8.7.1") |
// Opt in to process-wide defaults (none of these apply unless you set them)
Curl.DefaultMaxTimeSeconds = 30;
Curl.DefaultFollowRedirects = true;
var result = await Curl.ExecuteAsync("https://httpbin.org/redirect/1"); // now follows the redirect, with a 30 s cap
Without those two lines the same call returns the 302 itself and waits as long as the server takes, exactly like curl https://httpbin.org/redirect/1.
CurlResult
| Member | Type | Description |
|---|---|---|
StatusCode |
int |
HTTP status code |
IsSuccess |
bool |
true for 2xx |
Body |
string |
What curl would print to stdout: the response body as text, plus -v/-i/-I header text and -w output; empty with -o/-O |
Headers |
Dictionary<string,string> |
Response headers, case-insensitive keys |
BinaryData |
byte[] |
Response bytes for binary content |
IsBinary |
bool |
true when BinaryData is populated |
Timings |
CurlTimings |
NameLookup, Connect, AppConnect, PreTransfer, Redirect, StartTransfer, Total in milliseconds |
OutputFiles |
List<string> |
Files written by -o / -O |
Exception |
Exception |
Set when a request failed without throwing (the LibCurl instance API returns such a result instead of throwing) |
ParseJson<T>() / AsJson<T>() |
T |
Deserialize the body; property names match case-insensitively |
AsJsonDynamic() |
dynamic |
Parse without a type: a JsonDocument on .NET 8+ and .NET Framework, a Newtonsoft JObject on .NET Standard 2.0 |
GetHeader(name) / HasHeader(name) |
Case-insensitive header lookup | |
EnsureSuccess() |
CurlResult |
Throw CurlDotNet.Core.CurlHttpException when not 2xx |
SaveToFile(path) |
CurlResult |
Write body or bytes to disk |
ToStream() |
Stream |
Body as a stream |
var result = await Curl.ExecuteAsync("curl https://httpbin.org/json");
result.EnsureSuccess();
using var slideshow = System.Text.Json.JsonDocument.Parse(result.Body);
Console.WriteLine(slideshow.RootElement.GetProperty("slideshow").GetProperty("title"));
Console.WriteLine(result.GetHeader("content-type"));
Console.WriteLine($"{result.Timings.Total:F0} ms total");
CurlSettings
Per-call settings that apply on top of the command string.
using CurlDotNet.Core;
var settings = new CurlSettings()
.WithTimeout(30)
.WithFollowRedirects(true)
.WithRetries(count: 3, delayMs: 1000); // retry any failure 3 times, 1 s apart
var result = await Curl.ExecuteAsync("curl https://httpbin.org/status/503", settings);
Also applied: WithConnectTimeout(seconds), WithInsecure(), WithHeader(key, value), WithHeaders(dict), WithCancellation(token), WithProgress(callback), WithProxy(url[, user, pass]) (like -x/-U), WithUserAgent(ua) (like -A), WithCookies(container) (a CookieContainer that supplies the Cookie header and stores every Set-Cookie, redirect hops included), WithRedirectHandler(url => ...) (called for each followed redirect; throwing from it aborts with CurlAbortedByCallbackException, exit code 42), WithAutoDecompression(bool) (like --compressed) and WithBufferSize(bytes). Every member is applied to every URL of a multi-URL command, and a value set here overrides the same option in the command. WithRetries is the .NET shortcut for --retry N --retry-delay S --retry-all-errors; see curl –retry in C#.
CurlRequestBuilder reference
| Group | Methods |
|---|---|
| Create | Get, Post, Put, Delete, Patch, Head, Request(method, url) |
| Headers | WithHeader, WithHeaders, WithUserAgent, WithReferer |
| Body | WithData(string), WithJson(object), WithFormData(dict), WithMultipartForm(fields, files), WithFile(field, path), WithBinaryData(byte[]) |
| Auth | WithBasicAuth(user, pass), WithBearerToken(token), WithAuth(headerValue) |
| Timing | WithTimeout(TimeSpan), WithConnectTimeout(TimeSpan) |
| Redirects | FollowRedirects(bool = true), WithMaxRedirects(int) |
| SSL and proxy | Insecure(bool = true), WithProxy(url), WithProxy(url, user, pass) |
| Cookies | WithCookie(string) |
| Output | WithOutput(path), IncludeHeaders(), Verbose(), Compressed(), WithRange(range), AsBinary() |
| Run | Execute(), ExecuteAsync(CancellationToken = default), ToCurlCommand() |
var builder = CurlRequestBuilder.Post("https://httpbin.org/post")
.WithMultipartForm(
fields: new Dictionary<string, string> { ["title"] = "Report" },
files: new Dictionary<string, string> { ["file"] = "report.pdf" });
Console.WriteLine(builder.ToCurlCommand()); // curl -X POST -F 'title=Report' -F 'file=@report.pdf' ...
var result = await builder.ExecuteAsync();
LibCurl reference
Instance methods: GetAsync, PostAsync, PutAsync, PatchAsync, DeleteAsync, HeadAsync, each taking (url, Action<CurlOptions>? configure = null, CancellationToken ct = default); PostAsync, PutAsync and PatchAsync also take a data object.
Unlike the static Curl API, LibCurl does not throw for curl failures: it returns a CurlResult with StatusCode == 0 and the CurlException in result.Exception. A cancelled token still throws OperationCanceledException.
Configuration (chainable, applies to every request): WithHeader, WithBearerToken, WithBasicAuth, WithTimeout, WithConnectTimeout, WithFollowRedirects(maxRedirects), WithInsecureSsl(), WithProxy(url, user?, pass?), WithUserAgent, WithOutputFile, WithVerbose().
// Program.cs
builder.Services.AddSingleton(_ => new LibCurl()
.WithHeader("User-Agent", "MyApp/1.0")
.WithTimeout(TimeSpan.FromSeconds(15)));
// a service
public class GitHubService(LibCurl curl)
{
public Task<CurlResult> GetUserAsync(string login) =>
curl.GetAsync($"https://api.github.com/users/{login}");
}
CurlApiClient
A small wrapper for one REST base URL in the CurlDotNet namespace.
using CurlDotNet;
var api = new CurlApiClient("https://httpbin.org");
var created = await api.PostJsonAsync("/post", new { name = "Ada" });
var fetched = await api.GetAsync("/get");
var deleted = await api.DeleteAsync("/delete");
Also: PutJsonAsync(path, object), PatchJsonAsync(path, object).
String extension methods
In CurlDotNet.Extensions:
using CurlDotNet.Extensions;
var r1 = await "curl https://httpbin.org/get".CurlAsync();
var r2 = await "https://httpbin.org/get".CurlGetAsync();
var r3 = await "https://httpbin.org/image/png".CurlDownloadAsync("image.png");
Code generation and validation
var cmd = "curl -A 'MyApp/1.0' -H 'Accept: application/json' https://api.github.com/users/octocat";
Console.WriteLine(Curl.ToHttpClient(cmd)); // C# HttpClient code
Console.WriteLine(Curl.ToFetch(cmd)); // JavaScript fetch
Console.WriteLine(Curl.ToPythonRequests(cmd)); // Python requests
Console.WriteLine(Curl.ToPowershellCode(cmd)); // PowerShell Invoke-RestMethod
var validation = Curl.Validate(cmd); // parse only, no network
Exceptions
All network-level failures throw a subclass of CurlDotNet.Exceptions.CurlException, which carries CurlErrorCode (curl’s exit code), Command, a Suggestions list and StandardOutput (everything curl would have printed, including -w output). HTTP 4xx/5xx responses do not throw unless you call EnsureSuccess() or pass -f. A cancelled CancellationToken throws the standard OperationCanceledException, not a CurlException.
| Exception | When |
|---|---|
CurlTimeoutException (CurlOperationTimeoutException) |
-m / --max-time or connect timeout exceeded (exit 28) |
CurlHttpReturnedErrorException |
-f / --fail and HTTP status 400 or above (exit 22) |
CurlConnectionException, CurlCouldntConnectException |
Connection refused, reset or unreachable |
CurlDnsException, CurlCouldntResolveHostException |
Host name could not be resolved |
CurlSslException |
Certificate or TLS handshake failure |
CurlAuthenticationException |
Authentication could not be performed |
CurlProxyException |
Proxy could not be reached or rejected the request |
CurlMalformedUrlException, CurlUnsupportedProtocolException |
Bad URL or unsupported scheme |
CurlInvalidCommandException |
The command string could not be parsed |
CurlReadErrorException, CurlWriteErrorException |
An input file (-d @file, -T, -F) is missing (exit 26) or an output file cannot be written (exit 23) |
Full list with fixes: Exceptions. Every exit code: curl exit codes.