Skip to the content.

How to handle curl errors in C#

curl error handling in C# with CurlDotNet has two layers. HTTP error statuses (404, 500) come back as a normal CurlResult, so you check result.IsSuccess or call result.EnsureSuccess(). Transport failures (DNS, refused connection, timeout, too many redirects) throw a CurlDotNet.Exceptions.CurlException subclass, and its CurlErrorCode holds the curl CLI exit code.

dotnet add package CurlDotNet

The pattern

using CurlDotNet;
using CurlDotNet.Exceptions;

try
{
    var result = await Curl.ExecuteAsync("curl -m 10 https://httpbin.org/status/503");
    if (!result.IsSuccess)
        Console.WriteLine($"HTTP {result.StatusCode}: {result.Body}");
}
catch (CurlTimeoutException ex) { Console.WriteLine($"Timed out: {ex.Message}"); }
catch (CurlException ex)        { Console.WriteLine($"curl error {ex.CurlErrorCode}: {ex.Message}"); }

Turn HTTP errors into exceptions

EnsureSuccess() returns the result for chaining and throws for any status outside 200-299. It throws CurlDotNet.Core.CurlHttpException, which is not a CurlException. A different CurlDotNet.Exceptions.CurlHttpException also exists, so write the namespace out if you import both:

using CurlDotNet;

try
{
    var result = await Curl.ExecuteAsync("curl https://httpbin.org/status/404");
    result.EnsureSuccess();
}
catch (CurlDotNet.Core.CurlHttpException ex)
{
    Console.WriteLine($"{ex.StatusCode}: {ex.ResponseBody}");
}

EnsureStatus(201) and EnsureContains("text") work the same way.

Or do it the curl way: with -f / --fail, a status of 400 or above throws CurlHttpReturnedErrorException (exit code 22, a CurlDotNet.Exceptions.CurlHttpException) and the body is discarded. --fail-with-body throws the same exception but keeps the body:

using CurlDotNet;
using CurlDotNet.Exceptions;

try
{
    await Curl.ExecuteAsync("curl -f https://httpbin.org/status/404");
}
catch (CurlHttpReturnedErrorException ex)
{
    Console.WriteLine($"{ex.CurlErrorCode}: {ex.StatusCode}");  // 22: 404
}

The fluent builder

The builder throws the same exceptions (LibCurl is different: it returns a failed CurlResult with the exception in result.Exception, and throws only OperationCanceledException):

using CurlDotNet.Core;
using CurlDotNet.Exceptions;

try
{
    var result = await CurlRequestBuilder.Get("https://no-such-host.invalid/")
        .WithTimeout(TimeSpan.FromSeconds(5)).ExecuteAsync();
}
catch (CurlException ex) when (ex.IsRetryable())
{
    Console.WriteLine($"Transient ({ex.GetCurlErrorName()}), try again later.");
}

What is thrown when (observed)

Situation Exception CurlErrorCode
Host name does not resolve CurlCouldntResolveHostException 6
Proxy host does not resolve CurlCouldntResolveProxyException 5
Connection refused or unreachable CurlCouldntConnectException 7
TLS certificate rejected (no -k) CurlPeerFailedVerificationException (a CurlSslException) 60
TLS handshake failed CurlSslConnectErrorException (a CurlSslException) 35
Transfer not finished within -m (body included) CurlOperationTimeoutException (a CurlTimeoutException) 28
-f and a status of 400 or above CurlHttpReturnedErrorException 22
--max-redirs exceeded CurlTooManyRedirectsException 47
Unsupported scheme such as gopher:// CurlUnsupportedProtocolException 1
Command has no URL CurlInvalidCommandException 2
-F 'f=@missing.txt', -T missing.txt CurlReadErrorException 26
-o into a directory that does not exist CurlWriteErrorException 23
Server closed the connection without a reply CurlGotNothingException 52
Digest/NTLM negotiation failed CurlAuthErrorException 94
Your CancellationToken fired OperationCanceledException (standard .NET) n/a
4xx / 5xx response without -f nothing thrown; IsSuccess == false n/a

DNS and TLS failures have their own codes, so IsRetryable() is true for 6 and 7 but false for a certificate problem. The full list is in the curl exit code reference.

Useful members on CurlException

Member Use
CurlErrorCode curl exit code (int?)
GetCurlErrorName() Name for the code
Command The command that failed, exactly as written. It may contain secrets; log ToDetailedString() or ToJson() instead, which mask credentials
Suggestions Hints for fixing the problem, when available
IsRetryable() true for codes 6, 7, 28, 52, 55 and 56
ToUserFriendlyMessage() Text you can show to a user

Retrying

Add --retry <n> to the command. Like curl, it retries timeouts and HTTP 408, 429, 500, 502, 503 and 504, waits 1 second and doubles the wait each time (--retry-delay fixes it), and honours Retry-After. --retry-connrefused adds refused connections and --retry-all-errors retries any failure. When the retries run out, you get the last result or exception as usual:

using CurlDotNet;

var result = await Curl.ExecuteAsync("curl --retry 3 --retry-max-time 30 https://httpbin.org/get");
Console.WriteLine(result.StatusCode);

new CurlSettings().WithRetries(3, 1000) does the same from code, retrying any failure with a fixed delay in milliseconds. See curl –retry in C# for the details.

For your own policy (for example with Polly), retry on IsRetryable():

using CurlDotNet;
using CurlDotNet.Exceptions;

for (var attempt = 1; ; attempt++)
{
    try
    {
        var r = await Curl.ExecuteAsync("curl -m 5 https://httpbin.org/get");
        Console.WriteLine(r.StatusCode);
        break;
    }
    catch (CurlException ex) when (ex.IsRetryable() && attempt < 3)
    { await Task.Delay(TimeSpan.FromSeconds(attempt)); }
}