elsa-workflows/elsa-core · error · ProviderHttpException

RedirectRejected

RedirectRejected

Error message

ProviderHttpException(ProviderHttpFailure.RedirectRejected)

What it means

ProviderHttpClient deliberately disables automatic redirects (AllowAutoRedirect = false) so every hop can be revalidated against the outbound destination policy. This ProviderHttpException(ProviderHttpFailure.RedirectRejected) is thrown when a redirect is received but cannot be followed safely: the response kind is Token or UserInfo (security-sensitive endpoints must never redirect), the Location header is missing, or the maximum redirect count configured in ProviderEgress.MaximumRedirects has been exceeded.

Solutions

  1. Point your configuration at the final, direct URL for the endpoint (authority, token endpoint, userinfo endpoint) so no redirect is needed — especially for Token and UserInfo calls, which are always rejected on redirect.
  2. If redirects are legitimate for Discovery/SigningKeys requests, raise ProviderEgress.MaximumRedirects in ExternalAuthenticationOptions.
  3. Check for a redirect loop: the same host responding redirect→redirect means a server-side configuration problem; fetch the URL with curl -I to see the chain and fix the provider or the configured URL.
  4. Verify the Location target would pass outbound-destination validation; if the hop is to a disallowed host, allowlist that host or correct the provider's redirect target.

Example fix

// before — authority URL redirects to /.well-known/openid-configuration
options.Authority = new Uri("https://idp.example.com");

// after — use the final non-redirecting discovery root
options.Authority = new Uri("https://idp.example.com/realms/main");
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: detect redirects (and loops) for discovery URLs before using them
curl -sIL -o /dev/null -w "%{http_code} %{num_redirects} %{url_effective}" https://idp.example.com/.well-known/openid-configuration
// in C#: use HttpClientHandler(AllowAutoRedirect=false) to probe; reject if any 3xx appears for Token/UserInfo endpoints

Type guard

static bool IsRedirectRejectedCase(ProviderHttpException ex, ProviderResponseKind kind) =>
    ex.Failure == ProviderHttpFailure.RedirectRejected &&
    kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo;

Try / catch

try
{
    var response = await client.GetAsync(tokenEndpoint, ProviderResponseKind.Token, ct);
}
catch (ProviderHttpException ex) when (ex.Failure == ProviderHttpFailure.RedirectRejected)
{
    logger.LogWarning("Provider endpoint attempted a redirect; reconfigure to the final URL. Kind: {Kind}", kind);
    throw; // never auto-follow redirects for token/userinfo — that is the security boundary
}

Prevention

When it happens

Trigger: Calling IProviderHttpClient.GetAsync/PostFormAsync where the provider responds 301/302/303/307/308 and any of: kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo; the response has no Location header; more than ProviderEgress.MaximumRedirects redirects are returned; or a subsequent hop fails destination validation (which itself becomes DestinationRejected, not this error).

Common situations: Provider moving its token endpoint behind a redirect (common with provider migrations) — token/userinfo redirects are rejected outright; http→https or trailing-slash redirects on discovery documents causing redirect loops that exhaust MaximumRedirects; a misconfigured authority URL pointing at a redirecting landing page instead of the OIDC discovery document.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


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

Appendix: source

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

    private async ValueTask<ProviderHttpResponse> SendAsync(Uri uri, ProviderResponseKind kind, Func<Uri, HttpRequestMessage> createRequest, CancellationToken cancellationToken)
    {
        var redirects = 0;
        var current = uri;
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(options.Value.ProviderEgress.RequestTimeout);

        try
        {
            while (true)
            {
                await destinationValidator.ValidateAsync(current, timeout.Token);
                using var request = createRequest(current);
                using var response = await invoker.SendAsync(request, timeout.Token);

                if (IsRedirect(response.StatusCode))
                {
                    if (kind is ProviderResponseKind.Token or ProviderResponseKind.UserInfo || response.Headers.Location is null || redirects++ >= options.Value.ProviderEgress.MaximumRedirects)
                        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);

View on GitHub (pinned to fe9217bdfa)