Azure / azure-functions-durable-extension

Durable Task Framework extension for Azure Functions
MIT License
714 stars 270 forks source link

Support Custom Domains for Status/Admin endpoints #1026

Open dc799 opened 4 years ago

dc799 commented 4 years ago

Is your feature request related to a problem? Please describe. I'd like to be able to supply a custom domain to be used when creating the CheckStatusResponse message. Currently it appears to use the host from the incoming HttpRequestMessage's RequestUri.

The Location header for the 202 Accepted is then populated with the host name of the actual app service rather than the custom domain we have assigned to our Application Gateway.

Describe the solution you'd like Allow an Environment Variable to be supplied that will override the host name used for the status endpoint and other management endpoints.

Describe alternatives you've considered I've tried intercepting the incoming request in code and changing the host to be what I want. This works fine for the first response, but subsequent calls to the status endpoint will return the app service host name as I can't intercept and override the Request URI in there.

Additional context I'm calling a Durable Function via an Application Gateway. The App Gateway uses Path Based routing so the URLs look like:

cgillum commented 4 years ago

Thanks for raising this feature request.

Another workaround you could consider is implementing the status functions yourself as HTTP-triggered functions. For example (note that I'm using the Durable Functions 2.0 interfaces here):

[FunctionName("StartJob")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "jobs/{functionName}")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient client,
    string functionName,
    ILogger log)
{
    object eventData = await req.Content.ReadAsAsync<object>();
    string instanceId = await client.StartNewAsync(functionName, eventData);

    log.LogInformation($"Started job with ID = '{instanceId}'.");
    return CreateAcceptedResponse(req, client, instanceId);
}

[FunctionName("GetJobStatus")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = "jobs/{instanceId}")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient client,
    string instanceId)
{
    DurableOrchestrationStatus status = await client.GetStatusAsync(instanceId, false, false, false);
    if (status == null)
    {
        return req.CreateResponse(HttpStatusCode.NotFound);
    }

    switch (status.RuntimeStatus)
    {
        // The orchestration is running - return 202 w/Location header
        case OrchestrationRuntimeStatus.Running:
        case OrchestrationRuntimeStatus.Pending:
        case OrchestrationRuntimeStatus.ContinuedAsNew:
            return CreateAcceptedResponse(req, client, instanceId);
        default:
            return req.CreateResponse(HttpStatusCode.OK);
    }
}

private static HttpResponseMessage CreateAcceptedResponse(
    HttpRequestMessage req,
    IDurableOrchestrationClient client,
    string instanceId)
{
    var managementUrls = client.CreateHttpManagementPayload(instanceId);
    var response = req.CreateResponse(HttpStatusCode.Accepted);
    response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(5));

    // Override the hostname in the Location header with mygateway.com
    var uriBuilder = new UriBuilder(managementUrls.StatusQueryGetUri);
    uriBuilder.Host = "mygateway.com";
    response.Headers.Location = uriBuilder.Uri;
    return response;
}

The nice thing about this option is that it gives you full control over the API surface area - i.e. you could implement your own app-specific protocol rather than being forced into using the built-in protocols.

dc799 commented 4 years ago

@cgillum This is perfect, think I was sitting too close to the problem late last night to see this as an alternative! Perhaps my feature request isn't required...

I'll consider switching to 2.0. but we've had a few other scary experiences from being early adopters of the latest :)

wbittlehsl commented 2 years ago

@cgillum I hate to resurrect an old issue, but I'm also experiencing this. My situation is a little different. We have an Application Gateway in front of our Azure Function because the Azure Function has Private Endpoints configured for it. What this means is that the default URL of *.azurewebsites.net is simply not reachable from outside the VNet. The code above works great for the FIRST request - the one that triggers the orchestration.

To describe the issue better see the sequence below. Assume my function is called fn.azurewebsites.net on the VNet and called fn.test.com in the Application Gateway:

  1. The tool makes a request to fn.test.com
  2. The Application Gateway routes it to fn.azurewebsites.net
  3. The function begins the orchestration and using the code above returns a status URI/location header with fn.test.com as the host
  4. The tool uses the location header to get the status using fn.test.com
  5. The Application Gateway routes it to fn.azurewebsites.net
  6. The function processes the request (using the native management API because we didn't override this behavior) and returns a location header with fn.azurewebsites.net (instead of the correct host)
  7. The tool uses the location header to get the status using fn.azurewebsites.net
  8. The tool receives a 403 forbidden because the function is on a private network

I tried calling the custom status function using the durable function extension key, but that doesn't work either (401).

Is there a work around for this scenario?

wbittlehsl commented 2 years ago

If it helps anyone else, the missing piece was the AuthorizationLevel of the custom status function. It needs to be set to System instead of function so that the durable function extension key works for it.

jhofer commented 1 year ago

We are facing the same issue with isolated-worker azure functions. Currently we are replacing the base urls by ourself

cgillum commented 1 year ago

I added a help-wanted tag to this issue since I worry it be hard for the team to prioritize. In other words, we may need a community-contributed PR (or multiple PRs, since language-specific SDKs would need to be updated in some cases).

As for the fix, I believe the right way to approach this is to add some additional code in the various places where we generate these URLs to determine whether the request might have been forwarded from an HTTP gateway. There are a few different places that I'll list below where similar changes may be necessary:

Also, adding the contents of a TODO comment about this, which I think will be helpful:

// TODO: To better support scenarios involving proxies or application gateways, this
//       code should take the X-Forwarded-Host, X-Forwarded-Proto, and Forwarded HTTP
//       request headers into consideration and generate the base URL accordingly.
//       More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded.
//       One potential workaround is to set ASPNETCORE_FORWARDEDHEADERS_ENABLED to true.

In terms of priority, the built-in HTTP API is likely the highest since there's no manual workaround for it.