Calling a Cloud Function from another Cloud Function

36,717

Solution 1

You don't need to go through the trouble of invoking some shared functionality via a whole new HTTPS call. You can simply abstract away the common bits of code into a regular javascript function that gets called by either one. For example, you could modify the template helloWorld function like this:

var functions = require('firebase-functions');

exports.helloWorld = functions.https.onRequest((request, response) => {
  common(response)
})

exports.helloWorld2 = functions.https.onRequest((request, response) => {
  common(response)
})

function common(response) {
  response.send("Hello from a regular old function!");
}

These two functions will do exactly the same thing, but with different endpoints.

Solution 2

To answer the question, you can do an https request to call another cloud function:

export const callCloudFunction = async (functionName: string, data: {} = {}) => {
    let url = `https://us-central1-${config.firebase.projectId}.cloudfunctions.net/${functionName}`
    await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ data }),
    })
}

(Note we are using the npm package 'node-fetch' as our fetch implementation.)

And then simply call it:

callCloudFunction('search', { query: 'yo' })

There are legitimate reasons to do this. We used this to ping our search cloud function every minute and keep it running. This greatly lowers response latency for a few dollars a year.

Solution 3

It's possible to invoke another Google Cloud Function over HTTP by including an authorization token. It requires a primary HTTP request to calculate the token, which you then use when you call the actual Google Cloud Function that you want to run.

https://cloud.google.com/functions/docs/securing/authenticating#function-to-function

const {get} = require('axios');

// TODO(developer): set these values
const REGION = 'us-central1';
const PROJECT_ID = 'my-project-id';
const RECEIVING_FUNCTION = 'myFunction';

// Constants for setting up metadata server request
// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
const functionURL = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${RECEIVING_FUNCTION}`;
const metadataServerURL =
  'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=';
const tokenUrl = metadataServerURL + functionURL;

exports.callingFunction = async (req, res) => {
  // Fetch the token
  const tokenResponse = await get(tokenUrl, {
    headers: {
      'Metadata-Flavor': 'Google',
    },
  });
  const token = tokenResponse.data;

  // Provide the token in the request to the receiving function
  try {
    const functionResponse = await get(functionURL, {
      headers: {Authorization: `bearer ${token}`},
    });
    res.status(200).send(functionResponse.data);
  } catch (err) {
    console.error(err);
    res.status(500).send('An error occurred! See logs for more details.');
  }
};

October 2021 Update: You should not need to do this from a local development environment, thank you Aman James for clarifying this

Solution 4

Despite of the question tag and other answers concern the javascript I want to share the python example as it reflects the title and also authentification aspect mentioned in the question.

Google Cloud Function provide REST API interface what incluse call method that can be used in another Cloud Function. Although the documentation mention using Google-provided client libraries there is still non one for Cloud Function on Python.

And instead you need to use general Google API Client Libraries. [This is the python one].3

Probably, the main difficulties while using this approach is an understanding of authentification process. Generally you need provide two things to build a client service: credentials ans scopes.

The simpliest way to get credentials is relay on Application Default Credentials (ADC) library. The rigth documentation about that are:

  1. https://cloud.google.com/docs/authentication/production
  2. https://github.com/googleapis/google-api-python-client/blob/master/docs/auth.md

The place where to get scopes is the each REST API function documentation page. Like, OAuth scope: https://www.googleapis.com/auth/cloud-platform

The complete code example of calling 'hello-world' clound fucntion is below. Before run:

  1. Create default Cloud Function on GCP in your project.
  • Keep and notice the default service account to use
  • Keep the default body.
  1. Notice the project_id, function name, location where you deploy function.
  2. If you will call function outside Cloud Function environment (locally for instance) setup the environment variable GOOGLE_APPLICATION_CREDENTIALS according the doc mentioned above
  3. If you will call actualy from another Cloud Function you don't need to configure credentials at all.
from googleapiclient.discovery import build
from googleapiclient.discovery_cache.base import Cache
import google.auth

import pprint as pp

def get_cloud_function_api_service():
    class MemoryCache(Cache):
        _CACHE = {}

        def get(self, url):
            return MemoryCache._CACHE.get(url)

        def set(self, url, content):
            MemoryCache._CACHE[url] = content

    scopes = ['https://www.googleapis.com/auth/cloud-platform']

    # If the environment variable GOOGLE_APPLICATION_CREDENTIALS is set,
    # ADC uses the service account file that the variable points to.
    #
    # If the environment variable GOOGLE_APPLICATION_CREDENTIALS isn't set,
    # ADC uses the default service account that Compute Engine, Google Kubernetes Engine, App Engine, Cloud Run,
    # and Cloud Functions provide
    #
    # see more on https://cloud.google.com/docs/authentication/production
    credentials, project_id = google.auth.default(scopes)

    service = build('cloudfunctions', 'v1', credentials=credentials, cache=MemoryCache())
    return service


google_api_service = get_cloud_function_api_service()
name = 'projects/{project_id}/locations/us-central1/functions/function-1'
body = {
    'data': '{ "message": "It is awesome, you are develop on Stack Overflow language!"}' # json passed as a string
}
result_call = google_api_service.projects().locations().functions().call(name=name, body=body).execute()
pp.pprint(result_call)
# expected out out is:
# {'executionId': '3h4c8cb1kwe2', 'result': 'It is awesome, you are develop on Stack Overflow language!'}

Solution 5

These suggestions don't seem to work anymore.

To get this to work for me, I made calls from the client side using httpsCallable and imported the requests into postman. There were some other links to https://firebase.google.com/docs/functions/callable-reference there were helpful. But determining where the information was available took a bit of figuring out.

I wrote everything down here as it takes a bit of explaining and some examples.

https://www.tiftonpartners.com/post/call-google-cloud-function-from-another-cloud-function

Here's an inline version for the 'url' might expire.

This 'should' work, it's not tested but based off of what I wrote and tested for my own application.

module.exports = function(name,context) {
    const {protocol,headers} = context.rawRequest;
    const host = headers['x-forwardedfor-host'] || headers.host;
    // there will be two different paths for
    // production and development
    const url = `${protocol}://${host}/${name}`;
    const method = 'post';    
    const auth = headers.authorization;
    
    return (...rest) => {
        const data = JSON.stringify({data:rest});
        const config = {
            method, url, data,
            headers: {
               'Content-Type': 'application/json',
               'Authorization': auth,
               'Connection': 'keep-alive',
               'Pragma': 'no-cache,
               'Cache-control': 'no-cache',
            }
        };
        try {
            const {data:{result}} = await axios(config);
            return result;        
        } catch(e) {
            throw e;
        }
    }
}

This is how you would call this function.

const crud = httpsCallable('crud',context);
return await crud('read',...data);

context you get from the google cloud entry point and is the most important piece, it contains the JWT token needed to make the subsequent call to your cloud function (in my example its crud)

To define the other httpsCallable endpoint you would write an export statement as follows

exports.crud = functions.https.onCall(async (data, context) => {})

It should work just like magic.

Hopefully this helps.

Share:
36,717
ro-savage
Author by

ro-savage

Updated on October 07, 2021

Comments

  • ro-savage
    ro-savage over 2 years

    I am using a Cloud Function to call another Cloud Function on the free spark tier.

    Is there a special way to call another Cloud Function? Or do you just use a standard http request?

    I have tried calling the other function directly like so:

    exports.purchaseTicket = functions.https.onRequest((req, res) => {    
      fetch('https://us-central1-functions-****.cloudfunctions.net/validate')
        .then(response => response.json())
        .then(json => res.status(201).json(json))
    })
    

    But I get the error

    FetchError: request to https://us-central1-functions-****.cloudfunctions.net/validate failed, reason: getaddrinfo ENOTFOUND us-central1-functions-*****.cloudfunctions.net us-central1-functions-*****.cloudfunctions.net:443

    Which sounds like firebase is blocking the connection, despite it being a google owned, and therefore it shouldn't be locked

    the Spark plan only allows outbound network requests to Google owned services.

    How can I make use a Cloud Function to call another Cloud Function?

  • James Daniels
    James Daniels about 7 years
    If you do need "stepped" functions for any reason (say you're executing a series of third-party HTTP requests). I'd suggest publishing to Cloud Pub/Sub which could asynchronisly trigger a Cloud Pub/Sub function. HTTP triggers are sync, will timeout, and aren't "durable".
  • ro-savage
    ro-savage about 7 years
    Thanks Doug. That answers my first and main question. Are you able to tell me why I couldn't do a http call to *.cloudfunctions.net? The reason I was called the other cloud function was actually to simulate an external API call to an API we host on a non-google service.
  • Doug Stevenson
    Doug Stevenson about 7 years
    *.cloudfunctions.net is not currently whitelisted for network access in the free Spark tier. Whitelisting in general is to prevent casual abuse of non-google services, and you can think of *.cloudfunctions.net as non-google services (because developers like you actually share that entire space to provide your own).
  • Charlton Provatas
    Charlton Provatas over 6 years
    @DougStevenson At your earliest convenience would you mind taking a look stackoverflow.com/questions/47137368/… I would greatly appreciate. It is related to question.
  • Rajat Saxena
    Rajat Saxena over 6 years
    This is the exact approach I used in one of my apps.
  • James Tan
    James Tan about 5 years
    What if we are calling the function from another firebase function in another language such as golang?
  • Doug Stevenson
    Doug Stevenson about 5 years
    @JamesTan You can't share code between different languages deployed to different runtimes.
  • rnrneverdies
    rnrneverdies almost 4 years
    This doesn´t work if you are using an express application because express gets locked until fetch returns which causes fetch being locked since express never answer.
  • Maaz Bin Khawar
    Maaz Bin Khawar over 3 years
    What if someone deliberately wants to create another instance of an idempotent cloud function by calling it from itself to distribute long process into chunks to avoid timeouts.
  • Doug Stevenson
    Doug Stevenson over 3 years
    @MohammedMaaz If you have a new question, you should post it separately along with the code you have that doesn't work the way you expect.
  • Johnny Oshika
    Johnny Oshika almost 3 years
    This is great, thank you! Why did you choose to use compute metadata server instead of google-auth-library?
  • Johnny Oshika
    Johnny Oshika almost 3 years
    Well I couldn't get authentication to work with google-auth-library, but your code worked perfectly. The only thing I changed is updated the metadata server URL to http://metadata.google.internal/computeMetadata/v1/instance/‌​service-accounts/def‌​ault/identity?audien‌​ce=, which is what is documented cloud.google.com/functions/docs/securing/… and cloud.google.com/compute/docs/storing-retrieving-metadata.
  • Shea Hunter Belsky
    Shea Hunter Belsky almost 3 years
    @JohnnyOshika I wrote this a while ago so I can't quite remember why I did it this way instead of using google-auth-library. I think my thought process was "This way works, so let's make it work for now, and we can make it better later." I think using google-auth-library would be the preferred way for sure, rather than relying on an endpoint. I updated my answer to include the new URL, thanks!
  • VCD
    VCD almost 3 years
    I tried to use google-auth-library and it doesn't work with sample provided by Google. After some investigation I found that the sample is not correct. I filed an issue already.
  • GILO
    GILO over 2 years
    How do I send data to the function using this method?
  • Shea Hunter Belsky
    Shea Hunter Belsky over 2 years
    @GILO You would pass data as part of the request body when you call your function. Depending on if you're making a GET or POST request it'll be a little different, but you make it as part of that network request
  • MadMac
    MadMac over 2 years
    Works great, calling it from a trigger to update another project. Not using express.
  • Jeremy Walters
    Jeremy Walters over 2 years
    How would you go by calling another function locally?