elsa-workflows/elsa-core · error · ProviderHttpException

DestinationRejected

DestinationRejected

Error message

ProviderHttpException(ProviderHttpFailure.DestinationRejected)

What it means

SendAsync validates every request destination (including each redirect hop) through OutboundDestinationValidator. If the validator rejects the host, scheme, port, or proxy destination it throws OutboundDestinationException, which SendAsync translates into ProviderHttpException(ProviderHttpFailure.DestinationRejected). This is the library's SSRF/egress-control boundary: the configured provider URL points somewhere the deployment has not approved.

Solutions

  1. Update the configured provider/authority URL to an approved destination (public HTTPS host on the allowlist) and redeploy.
  2. Add the provider's host to the outbound-destination allowlist in your ExternalAuthentication egress configuration if the destination is legitimate.
  3. Inspect where the redirect chain lands (curl -I each hop) and either allowlist the final host or fix the provider's redirect target.
  4. Check the DNS resolution: if a public hostname resolves to a private IP, fix DNS or update validator policy deliberately — do not disable validation.

Example fix

// before — internal endpoint rejected by destination validation
await client.GetAsync(new Uri("http://localhost:8080/.well-known/openid-configuration"), ProviderResponseKind.Discovery);

// after — approved HTTPS provider host
await client.GetAsync(new Uri("https://idp.example.com/.well-known/openid-configuration"), ProviderResponseKind.Discovery);
Defensive patterns

Strategy: validation

Validate before calling

// validate provider URLs against your egress policy before configuring
var uri = new Uri(candidateUrl);
bool isAllowed = uri.Scheme == Uri.UriSchemeHttps
    && allowedHosts.Contains(uri.Host)
    && !IPAddress.TryParse(uri.Host, out var ip) || !IsPrivateIp(ip!);
if (!isAllowed) throw new InvalidOperationException($"Provider URL not on approved egress list: {candidateUrl}");

Type guard

static bool IsApprovedDestination(Uri uri, IReadOnlySet<string> allowedHosts) =>
    uri.Scheme == Uri.UriSchemeHttps && allowedHosts.Contains(uri.Host);

Try / catch

try
{
    var response = await client.GetAsync(discoveryUri, ProviderResponseKind.Discovery, ct);
}
catch (ProviderHttpException ex) when (ex.Failure == ProviderHttpFailure.DestinationRejected)
{
    logger.LogError("Destination rejected by outbound validation: {Uri}", discoveryUri);
    throw; // do not retry — egress policy rejection is deterministic, not transient
}

Prevention

When it happens

Trigger: Calling GetAsync/PostFormAsync with a URI whose scheme/host/port is not allowed by OutboundDestinationValidator policy (e.g. non-HTTPS, private/loopback IP, host not on the allowlist), or a redirect hop landing on a disallowed destination; also thrown when a configured proxy URI fails ValidateApprovedProxy checks during a request.

Common situations: Typing an internal/dev provider URL (http://localhost:8080, http://10.x.x.x) into production config where private egress is blocked; OIDC discovery document advertising endpoints on hosts outside the approved egress list; redirect from the provider to a different domain that is not allowlisted; DNS rebinding or a host resolving to a private IP caught by the validator.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

                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);
        }
        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);

View on GitHub (pinned to fe9217bdfa)