Handle curl errors and exceptions in C#
This recipe shows how to handle curl errors and exceptions in C# with CurlDotNet. There are two kinds of failure: HTTP errors (the server answered, but with 4xx or 5xx) come back as a normal result you inspect, unless you ask for an exception with -f or EnsureSuccess(); transport errors (timeout, DNS, refused connection, TLS, too many redirects) are thrown as typed exceptions that carry curl’s exit code. Cancelling your own CancellationToken throws the standard OperationCanceledException.
The problem
A request can fail in many different ways and you want code that tells them apart: retry a 503, give up on a 404, report a timeout, and never swallow a bad URL silently.
Solution 1: check the result for HTTP errors
Like curl without -f, Curl.ExecuteAsync does not throw for HTTP 4xx/5xx. You get a CurlResult with IsSuccess == false and the real StatusCode, body and headers.
using CurlDotNet;
var result = await Curl.ExecuteAsync("curl https://httpbin.org/status/404");
if (!result.IsSuccess)
{
Console.WriteLine($"HTTP {result.StatusCode}"); // HTTP 404
Console.WriteLine(result.Body); // error body, if any
}
IsSuccess is true for any 2xx status. For a specific code use result.StatusCode == 201 or result.EnsureStatus(201).
Turn HTTP errors into exceptions with EnsureSuccess
When a non-2xx answer should abort, call EnsureSuccess(). It throws CurlDotNet.Core.CurlHttpException carrying StatusCode, ResponseBody and ResponseHeaders, and returns the result otherwise so you can chain.
using CurlDotNet;
using CurlDotNet.Core;
try
{
var user = (await Curl.ExecuteAsync("curl https://api.github.com/users/octocat"))
.EnsureSuccess()
.AsJsonDynamic();
Console.WriteLine(user.login);
}
catch (CurlHttpException ex)
{
Console.WriteLine($"Server answered {ex.StatusCode}: {ex.ResponseBody}");
}
A CurlHttpException type also exists in CurlDotNet.Exceptions. If a file has both using CurlDotNet.Core; and using CurlDotNet.Exceptions;, write catch (CurlDotNet.Core.CurlHttpException ex) to avoid an ambiguous-reference compile error.
Fail on HTTP errors the curl way (-f)
-f / --fail makes an HTTP status of 400 or above an error, as in curl (exit code 22). CurlDotNet throws CurlHttpReturnedErrorException (from CurlDotNet.Exceptions), which carries StatusCode. With -f the error body is discarded; --fail-with-body keeps it in ResponseBody.
using CurlDotNet;
using CurlDotNet.Exceptions;
try
{
await Curl.ExecuteAsync("curl --fail-with-body https://httpbin.org/status/404");
}
catch (CurlHttpReturnedErrorException ex)
{
Console.WriteLine($"HTTP {ex.StatusCode}, exit code {ex.CurlErrorCode}"); // HTTP 404, exit code 22
Console.WriteLine(ex.ResponseBody); // kept by --fail-with-body
}
Solution 2: catch transport exceptions
Every failure the library reports derives from CurlDotNet.Exceptions.CurlException, and CurlErrorCode holds the curl exit code the real curl would have returned. Catch the specific types first and the base type last. (Cancellation is the one exception: it is the standard OperationCanceledException, see below.)
using CurlDotNet;
using CurlDotNet.Exceptions;
try
{
var result = await Curl.ExecuteAsync("curl -m 5 -L https://httpbin.org/delay/10");
}
catch (CurlOperationTimeoutException ex) { Console.WriteLine($"Timed out after {ex.Timeout}"); }
catch (CurlCouldntResolveHostException ex) { Console.WriteLine($"Unknown host {ex.Hostname}"); }
catch (CurlCouldntConnectException ex) { Console.WriteLine($"Cannot reach {ex.Host}:{ex.Port}"); }
catch (CurlTooManyRedirectsException ex) { Console.WriteLine($"{ex.RedirectCount} redirects"); }
catch (CurlMalformedUrlException ex) { Console.WriteLine($"Bad URL: {ex.MalformedUrl}"); }
catch (CurlException ex) { Console.WriteLine($"curl error {ex.CurlErrorCode}: {ex.Message}"); }
The ones you meet most often:
| Exception | Exit code | When it is thrown |
|---|---|---|
CurlException with code 2 |
2 | Conflicting options such as -I with -d, or an option missing its value. |
CurlMalformedUrlException |
3 | The URL (or a URL glob) could not be parsed. Has MalformedUrl. |
CurlCouldntResolveProxyException |
5 | The proxy host name could not be resolved. |
CurlCouldntResolveHostException |
6 | The host name could not be resolved. Has Hostname. |
CurlCouldntConnectException |
7 | The connection was refused or failed (directly or through the proxy). Has Host and Port. |
CurlHttpReturnedErrorException |
22 | -f / --fail-with-body and the server answered 400 or above. |
CurlWriteErrorException |
23 | An -o file or directory could not be written. |
CurlReadErrorException |
26 | A file named by -d @file, --data-binary @file, -F name=@file or -T file could not be read. |
CurlOperationTimeoutException |
28 | -m / --max-time or --connect-timeout expired. Derives from CurlTimeoutException (Timeout property). |
CurlSslConnectErrorException, CurlPeerFailedVerificationException |
35, 60 | The TLS handshake failed, or the certificate was rejected. |
CurlTooManyRedirectsException |
47 | -L followed more redirects than --max-redirs allows. Has RedirectCount. |
CurlAuthErrorException |
94 | Digest/NTLM negotiation failed. A wrong password is a normal 401 result instead. |
The full list, with every curl exit code, is in the curl exit code reference.
Use the Suggestions on the exception
Every CurlException carries a Suggestions list and a DiagnosticInfo string meant for logs and error pages, plus the Command that failed and the curl exit code in CurlErrorCode.
catch (CurlException ex)
{
Console.WriteLine(ex.Message);
foreach (var tip in ex.Suggestions)
Console.WriteLine($" - {tip}");
Console.WriteLine(ex.DiagnosticInfo);
}
Cancellation
Pass a CancellationToken. When it fires you get the standard .NET OperationCanceledException (a TaskCanceledException is one), the same as any other async API, so it is easy to tell apart from a -m timeout.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await Curl.ExecuteAsync("curl https://httpbin.org/delay/10", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Cancelled by the caller");
}
See the cancellation tokens tutorial.
Retrying transient failures
curl’s --retry N works as it does in curl: it retries timeouts and HTTP 408, 429, 500, 502, 503 and 504, waits 1 second and doubles the wait each time (or uses --retry-delay), and honours a Retry-After header. --retry-connrefused also retries refused connections, and --retry-all-errors retries any failure.
var result = await Curl.ExecuteAsync(
"curl --retry 3 --retry-max-time 60 https://httpbin.org/status/503");
Console.WriteLine(result.StatusCode); // 503 after the last attempt (no -f, so no exception)
From C#, CurlSettings.WithRetries(count, delayMs) turns retries on for any command; it retries every kind of failure (like --retry-all-errors) with a fixed delay:
using CurlDotNet.Core;
var settings = new CurlSettings().WithRetries(3, delayMs: 500);
var result = await Curl.ExecuteAsync("curl https://httpbin.org/get", settings);
See curl –retry in C# for the details. If you need your own policy (for example only 5xx and timeouts, with jitter), a hand-written loop still works:
static async Task<CurlResult> ExecuteWithRetries(string command, int attempts = 3)
{
for (var i = 1; ; i++)
{
try
{
var result = await Curl.ExecuteAsync(command);
if (result.IsSuccess || result.StatusCode < 500 || i == attempts) return result;
}
catch (CurlOperationTimeoutException) when (i < attempts) { }
catch (CurlCouldntConnectException) when (i < attempts) { }
await Task.Delay(TimeSpan.FromMilliseconds(500 * Math.Pow(2, i - 1)));
}
}
Do not retry 4xx responses: the request itself is wrong and will fail the same way again.
Builder variant
The request builder returns the same CurlResult and throws the same exceptions.
using CurlDotNet.Core;
using CurlDotNet.Exceptions;
try
{
var result = await CurlRequestBuilder
.Get("https://httpbin.org/status/500")
.WithTimeout(TimeSpan.FromSeconds(10))
.ExecuteAsync();
result.EnsureSuccess();
}
catch (CurlDotNet.Core.CurlHttpException ex) { Console.WriteLine($"HTTP {ex.StatusCode}"); }
catch (CurlException ex) { Console.WriteLine(ex.Message); }
Troubleshooting
My catch (CurlHttpException) never runs on a 404. HTTP errors are results, not exceptions, unless you call EnsureSuccess() or EnsureStatus(), or add -f / --fail-with-body to the command (then catch CurlHttpReturnedErrorException).
Cancellation versus timeout. Your own CancellationToken throws OperationCanceledException; -m / --max-time throws CurlOperationTimeoutException (exit code 28). Catch them separately to tell “the caller gave up” from “the server was too slow”.
Ambiguous reference CurlHttpException. Both CurlDotNet.Core and CurlDotNet.Exceptions define one. Qualify the type: catch (CurlDotNet.Core.CurlHttpException ex).
A self-signed certificate fails with CurlPeerFailedVerificationException. Certificate problems are TLS exceptions (exit code 60; a failed handshake is CurlSslConnectErrorException, exit code 35), not connection failures. For development only, add -k (or .Insecure() on the builder). For production, trust the CA instead: --cacert ca.pem (and client certificates with --cert) work as in curl on .NET 5 and later; see SSL errors.
I want to validate the command before running it. Curl.Validate("curl ...") returns a ValidationResult with IsValid and Error without sending anything. A misspelled option makes it invalid.