Azure-functions-host: Enabling CORS for local development

Created on 29 Apr 2019  路  21Comments  路  Source: Azure/azure-functions-host

I am able to enable CORS for local development using a local.settings.json file and running the project from Visual Studio. However, the docker image uses the runtime to host the project, which ignores local.settings.json.

Is there an environment variable that I can set instead to make the runtime use CORS? Or is there another approach I should use?

needs-discussion

Most helpful comment

setting CORS_ALLOWED_ORIGINS='["http://localhost","http://some.other.origin"]' -e CORS_SUPPORT_CREDENTIALS=true COULD work.

However it appears that the CORS middleware is only run if(environment.IsLinuxConsumption()) which doesn't seem applicable to a self-hosting use case.

Would it be reasonable to add an environment.ShouldRunCors() extension method to check the values of CORS_ALLOWED_ORIGINS? That would be called in WebScriptHostBuilderExtensions.cs and CorsOptionsSetup.cs.

I don't really have the full context how what else might be broken by that. Is this a viable solution?

All 21 comments

@ahmelsayed I suspect you helped enable this for the core tools when running local. Any idea what would be required to enable this in the docker scenarios?

I also encountered this issue from my docker container and I've tried proxies.json but no luck, the only thing that seemed to work was actually modifying the request but this isn't a good solution as it would require all your functions to call a method to set the headers

public IActionResult Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", "options", Route = "negotiate")]HttpRequest req,
    [SignalRConnectionInfo(HubName = "myHub", UserId = "<userId>")]SignalRConnectionInfo connectionInfo,
    ILogger log)
{
    try
    {
        if (!req.HttpContext.Response.Headers.ContainsKey("Access-Control-Allow-Credentials"))
        {
            req.HttpContext.Response.Headers.Add("Access-Control-Allow-Credentials", "true");
        }
        if (req.Headers.ContainsKey("Origin") && !req.HttpContext.Response.Headers.ContainsKey("Access-Control-Allow-Origin"))
        {
            req.HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", req.Headers["Origin"][0]);
        }
        if (req.Headers.ContainsKey("Access-Control-Request-Headers"))
        {
            req.HttpContext.Response.Headers.Add("Access-Control-Allow-Headers", req.Headers["access-control-request-headers"][0]);
        }
        log.LogInformation("negotiate API succeeded.");
        if (connectionInfo == null)
        {
            return new NotFoundObjectResult("Azure SignalR not found.");
        }
        return new OkObjectResult(connectionInfo);
    }
    catch
    {
        return new BadRequestResult();
    }
}

@ahmelsayed - do you mind adding some details to what needs to be done from the host side to enable CORS in a docker image?

Adding @kashimiz

Not sure if we settled on how/where the cors config would be specified when running the functions host outside app service. Assuming it is host.json we need the following

1) schema changes in host.json to include cors config
2) Specialize the cors middleware in the functions runtime with the specified cors config on container startup.

Core tools might also be able to work with the same host.json config going forward.

Indeed, with the new capabilities recently added, this scenario is only missing the configuration source for the CORS settings when running outside of App Service. Flagging this for discussion so we can decide how to properly expose this in a way that would compose well with the hosted scenario.

Any word on this or a recommended workaround in the meantime?

I guess a workaround for now would be to do your development directly on the debian docker instance that is being made available here and is presumably the same as the production runtime. Then on that do func start instead of building and starting the docker instance. Using the function development runtime (what fires up on func start) builds faster than completely rebuilding the docker image anyway so you could argue it's a better development experience.

Then your dev docker box has the same profile of what your Linux function code will ultimately get deployed to, so problem solved I guess.

I haven't tried this but seems plausible. For me this would mean changing my whole development toolchain over to debian just to solve the CORS issue so I'm not that eager to jump on it just yet. Would prefer to just fire up the docker instance to develop and test my function code only.

@anthonychu - I remember I've seen you enable CORS locally with local.settings.json. Do you know if/where that's documented anywhere? And am I remembering right that this is possible?

Yes. It's here: https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash#local-settings-file

But it sounds like what we're looking for is to enable CORS in the host (without Core Tools). @kashimiz might know if this is possible. We do read the CORS config from environment variables in the host:

https://github.com/Azure/azure-functions-host/blob/d648f761260e94af80853283f98023c56c2aa6df/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs#L95-L97

https://github.com/Azure/azure-functions-host/blob/8df66d1bf7cfe4ffafdc02870275928c4be07519/src/WebJobs.Script.WebHost/Configuration/HostCorsOptionsSetup.cs#L21-L34

Haven't done this but given what I see here, I'd try something like: -e CORS_ALLOWED_ORIGINS='["http://localhost","http://some.other.origin"]' -e CORS_SUPPORT_CREDENTIALS=true but I could be waaay off.

@anthonychu was easy for me to give your idea a shot. I added the two environment vars to my docker run using "*" and I'm still getting the same CORS error: No 'Access-Control-Allow-Origin' header is present on the requested resource.. Looks like @vnom2112 had the same idea: https://github.com/Azure/azure-functions-docker/issues/187. And it didn't work for him either.

@steveellis A couple of things to note... If you enable credentials support, you cannot use *, you must specify the actual origin(s). Also looks like the origins list needs to be a JSON serialized array of strings. (not saying this is going to work though, but make sure you take these details into account if you try setting the variables)

setting CORS_ALLOWED_ORIGINS='["http://localhost","http://some.other.origin"]' -e CORS_SUPPORT_CREDENTIALS=true COULD work.

However it appears that the CORS middleware is only run if(environment.IsLinuxConsumption()) which doesn't seem applicable to a self-hosting use case.

Would it be reasonable to add an environment.ShouldRunCors() extension method to check the values of CORS_ALLOWED_ORIGINS? That would be called in WebScriptHostBuilderExtensions.cs and CorsOptionsSetup.cs.

I don't really have the full context how what else might be broken by that. Is this a viable solution?

@kashimiz - do you know if the above is a viable solution or does it need more exploration?

We're having the same issue. We're trying to package our function apps in docker containers.

if(test-path Dockerfile){del DockerFile}
func init --docker-only
Add-Content -Path Dockerfile -Value `n'ADD host_secret.json /azure-functions-host/Secrets/host.json'
docker build -f Dockerfile -t <imagename> .
docker run -p 7071:80 <imagename>

We just have our QA testers do a git pull and fire off the above script.

The following didn't would work for us, but it looks like it's not implemented yet?
docker run -p 7071:80 -e CORS_ALLOWED_ORIGINS='["http://localhost:4200"]' -e CORS_SUPPORT_CREDENTIALS=true <imagename>

Any other suggestions?

Update: I ended up adding an Nginx reverse proxy container to our "solution". It feels clunky though for what I'm getting. Would prefer to have Cors enabled and configurable on the function app container.

@mrzachhigginsofficial this is what I ended up doing too, there are nginx ingress annotations you can add to enable CORS and supply a list of domains

I have also implemented an Nginx reverse proxy for this. Native ability would be much cleaner though!

We're also facing this issue, too bad.
After much fighting and digging, I finally found this thread.

@MattJeanes @KillianW @mrzachhigginsofficial , if you have a small gist/snippet of your Nginx setup at hand, it would be very appreciated.

@0gust1 We use ingress-nginx + Helm charts on Kubernetes for our deployments, but here is an example that should get you on your way if this is your scenario:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .app.name }}-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    {{- if .app.cors }}
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "{{ tpl (join "," .app.cors) . }}"
    {{- end }}

Or a resolved template example:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://mysite.com,https://myothersite.com"

We're also facing this issue, too bad.
After much fighting and digging, I finally found this thread.

@MattJeanes @KillianW @mrzachhigginsofficial , if you have a small gist/snippet of your Nginx setup at hand, it would be very appreciated.

If you're looking to run this locally for some testing or just want to see how the build works locally... This is part of the documentation I wrote earlier this year. We ended up not using the docker container too much for various reasons. Not sure if this is the best way to do it, but it does work. Step 1 creates the docker file and builds/runs the function app image. Step 2 builds and runs the reverse proxy.

:heavy_check_mark: Create Docker Image and Container (Script + Reverse Proxy)

  1. Run the following script from a Powershell terminal in the CSPROJ directory:
docker container rm dream-dashboardapi

if(test-path Dockerfile){del DockerFile}

func init --docker-only
Add-Content -Path Dockerfile -Value `n'ADD host_secret.json /azure-functions-host/Secrets/host.json'
Add-Content -Path Dockerfile -Value 'ENV AzureWebJobsSecretStorageType=files'
docker build -f Dockerfile -t dream-dashboardapi:latest .
docker run -p 8080:80 --name dream-dashboardapi dream-dashboardapi:latest
  1. Run the following script from a Powershell terminal in the CSPROJ directory:
docker container rm dream-reverseproxy
docker build -f nginx_Dockerfile -t dream-reverseproxy:latest .
docker run -p 7071:80 --name dream-reverseproxy dream-reverseproxy:latest

And the DockerFile is as follows (you can put this in your repo and give testers a script based on the above):

{
    listen       80;
    server_name  localhost;

    location /api/ 
    {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://host.docker.internal:8080/api/;

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            #
            # Custom headers and headers various browsers *should* be OK with but aren't
            #
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            #
            # Tell client that this pre-flight info is valid for 20 days
            #
            add_header 'Access-Control-Max-Age' 1728000;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }
        if ($request_method = 'POST') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
        if ($request_method = 'GET') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
        }
    }        
}

Thanks a lot, very helpful !

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mathewc picture mathewc  路  4Comments

JasonBSteele picture JasonBSteele  路  3Comments

paulbatum picture paulbatum  路  4Comments

christopheranderson picture christopheranderson  路  4Comments

justinyoo picture justinyoo  路  3Comments