elsa-workflows/elsa-core · error · ProviderHttpException

Timeout

Timeout

Error message

ProviderHttpException(ProviderHttpFailure.Timeout)

What it means

Each provider request runs under a linked CancellationTokenSource cancelled after ProviderEgress.RequestTimeout. If that internal timeout fires (the caller's own token is NOT cancelled — hence the when filter), SendAsync throws ProviderHttpException(ProviderHttpFailure.Timeout). It distinguishes the library's per-request timeout from caller-initiated cancellation, which propagates as OperationCanceledException instead.

Solutions

  1. Increase ProviderEgress.RequestTimeout in ExternalAuthenticationOptions if the provider legitimately needs longer (e.g. 30s instead of the default).
  2. Retry the request with backoff — timeouts are often transient; wrap the call in a Polly-style retry or your own loop.
  3. Diagnose the provider/network: curl -w with timing against the endpoint to see if DNS, connect, TLS, or server response is the slow stage; check provider status pages.
  4. Check ConnectTimeout as well — a stalled TCP connection is bounded by ConnectTimeout, so raising RequestTimeout alone may not help connection-level stalls.

Example fix

// before
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5)); // request timeout too tight

// after — configured via options
services.AddOptions<ExternalAuthenticationOptions>()
    .Configure(o => o.ProviderEgress.RequestTimeout = TimeSpan.FromSeconds(30));
Defensive patterns

Strategy: retry

Validate before calling

// pre-flight reachability probe with explicit timing before using a provider endpoint
using var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var sw = Stopwatch.StartNew();
var ok = (await probe.GetAsync(providerUrl)).IsSuccessStatusCode;
if (!ok || sw.Elapsed > TimeSpan.FromSeconds(5))
    logger.LogWarning("Provider endpoint slow or unreachable ({Elapsed}ms): {Url}", sw.ElapsedMilliseconds, providerUrl);

Type guard

static bool IsEgressTimeout(ProviderHttpException ex) => ex.Failure == ProviderHttpFailure.Timeout;

Try / catch

for (var attempt = 1; attempt <= 3; attempt++)
{
    try
    {
        return await client.GetAsync(uri, kind, ct);
    }
    catch (ProviderHttpException ex) when (ex.Failure == ProviderHttpFailure.Timeout && attempt < 3)
    {
        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct); // exponential backoff
    }
}

Prevention

When it happens

Trigger: Calling GetAsync/PostFormAsync against a provider that takes longer than ProviderEgress.RequestTimeout to respond — slow token endpoint, hanging TLS handshake/connection (also bounded by ConnectTimeout), or a discovery endpoint stalled mid-response. Thrown only when the elapsed timeout triggered the cancellation and the caller's token is still live.

Common situations: Provider under load or rate-limiting delaying responses beyond the default timeout; network firewall silently dropping packets so the connection hangs until timeout; oversized/slow discovery or JWKS responses on high-latency links; misjudging the timeout budget when chaining token + userinfo calls.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/18d1eafdc57bf086. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.ExternalAuthentication/Services/ProviderHttpClientFactory.cs:118

                        throw new ProviderHttpException(ProviderHttpFailure.RedirectRejected);

                    current = new(current, response.Headers.Location);
                    continue;
                }

                if (!response.IsSuccessStatusCode)
                    return new(response.StatusCode, []);

                return new(response.StatusCode, await ReadResponseBodyAsync(response, kind, timeout.Token));
            }
        }
        catch (OutboundDestinationException)
        {
            throw new ProviderHttpException(ProviderHttpFailure.DestinationRejected);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new ProviderHttpException(ProviderHttpFailure.Timeout);
        }
        catch (ProviderHttpException)
        {
            throw;
        }
        catch (Exception) when (!cancellationToken.IsCancellationRequested)
        {
            throw new ProviderHttpException(ProviderHttpFailure.TransportFailure);
        }
    }

    private async Task<byte[]> ReadResponseBodyAsync(HttpResponseMessage response, ProviderResponseKind kind, CancellationToken cancellationToken)
    {
        var limit = GetResponseLimit(kind);
        var contentLength = response.Content.Headers.ContentLength;
        if (contentLength is not null && contentLength > limit)
            throw new ProviderHttpException(ProviderHttpFailure.ResponseTooLarge);

View on GitHub (pinned to fe9217bdfa)