DEV Community

MartinJ
MartinJ

Posted on

Code templates for Firebase onRequest and onCall Functions

Code templates for Firebase onRequest and onCall Functions

Composition VIII - Wassily Kandinsky
Composition VIII - Wassily Kandinsky

Last reviewed: April 2024

Introduction

Google Cloud functions are soooooooo useful! But they're a wee bit tricky until you get your eye in. Some simple templates may be handy.

Just to set the scene, here's where you might use a Cloud function:

  1. Heavyweight processing that would make your webapp look like a dog if you tried it in the browser
  2. Processing that uses security certificates (eg Paypal keys). Exposing these in a webapp would be disastrous
  3. Sharing code between applications in different domains.

Cloud functions come in several different forms. This post concentrates on Google's general-purpose onRequest function class and its more refined onCall offspring.

If you're new to this stuff, the best way of getting a handle on Cloud functions may be to recall the days when you might have used your browser to poke your remote ISP with a URL along the lines of:

https://targetSite.com?param1="1"&param2="2"

As you'll remember, this fires up a page at https://targetSite.com that does something with values "1" and "2" in its param1 and param2 fields. In my own experience, this would likely have been a PHP page on Hostpapa that returned a stack of HTML. Oh happy days (not!)

You'll also recall how you might have coded up an HTML <form> tag to launch this sort of URL reference programmatically from your webapp with parameters from your form's <input> fields. Then, a little down the road to becoming a webapp ninja, you'd have learnt how you can use the Javascript Fetch function to bypass the <form> arrangement altogether and submit parameter data to the URL under your own control.

Google Cloud functions provide the Cloud equivalent of ISP pages. They have publicly addressable URLs. The difference is that they come with inbuilt safeguards to ensure that they're only accessible by authorised users and are also a bit more refined as regards how they handle their input and output.

The onRequest function

Here's a commented example of an onRequest function that receives data in its "req" parameter and returns it in "res".

exports.selectViaOnRequestFunction = functions.region('europe-west2').https.onRequest(async (req, res) => {

    // The 'europe-west2' bit above is where you would declare your own
    // function's location - leave this out completely if you're happy
    // to use the default US location

    // In order to appreciate the next bit of code, you need to
    // understand that, once your function is on the web, anybody who
    // knows its url can access it and feed it with parameters to suit
    // their own ends.  This is because Firestore functions run under
    // project account privileges - where user-base Firestore rules 
    // are bypassed. As a result it now become critically important 
    // to ensure that calls only come from domains that you authorise. 
    // The technology that achieves this is called CORS - (Cross-Origin 
    // Resource Sharing).

    // The CORS mechanism is based on the use of 'headers" - packets of
    // information that the web's architecture enables you to wrap around
    // your transactions. CORS requires a Cloud function to use headers
    // to perform a little dance before accepting a request. On first
    // receipt of a call (the so-called "pre-flight" stage), the function
    // must respond with a header that it can use to tell the caller
    // which domains it's prepared to accept calls from. Unless Fetch
    // find that this matches the caller's domain, it will terminate the
    // dance with a CORS "unauthorised domain" error. You can see the
    // first stage of this below where the function looks for a value of
    // "OPTIONS" in req.method. This tells it that it's got a new caller 
    // and must therefore respond with an authorisation header. 

    if (req.method === 'OPTIONS') {

    // Here's where the function tells Fetch that it's prepared to receive 
    // calls from 'https://approvedSite.org'

        res.set('Access-Control-Allow-Origin', 'https://approvedSite.org');

    // Telling Fetch which domains can be used is only the half of it.
    // Fetch also needs to be told which 'calling methods' (GET, POST
    // etc) and header types (eg 'Content-type') may be used. In the
    // example below, the function authorises use of the POST method and
    // the "content type" header - this will later be used in a Fetch
    // call to send a stringified JSON object using a "'Content-Type':
    // 'application/json'" header

        res.set('Access-Control-Allow-Methods', 'POST');
        res.set('Access-Control-Allow-Headers', 'Content-Type'); 

    // The pre-flight call concludes with a "res.status(204).send('');"
    // that explicitly states that this is an empty response.

        res.status(204).send('');

    } else {

        // Here's the point of entry for a request that has now passed
        // the pre-flight call and so is warranted an authorised source
        // using authorised methods and content-type. Note that this
        // second entry is still required to include an
        // 'Access-Control-Allow-Origin' header in its response

           res.set('Access-Control-Allow-Origin', 'https://approvedSite.org');

        // Tip: make sure you've got your function processing well-logged
        // in this section because processing errors here may still be
        // flagged as CORS errors in your calling code. Check the
        // function logs in the Cloud console to see what's going on 

        // Now get your input from req.body. Although, as you'll see
        // later, you have to "stringy" your parameter object (ie
        // turn it into a character-based string so that it can be
        // transmitted as a POST request), this is converted back into an
        // object automatically by onRequest  - don't try JSON.parse on
        // this - it will error!

        const searchParams = req.body 

        // Now do your processing to construct a 'result' object. If you
        // wanted to apply user-restrictions here, you'd achieve this by
        // including user details in your request body. 

       try {

        // Finally, return your response to the calling program. Note
        // that if you're returning an object there's no need to
        // 'stringify' it - the onRequest function does this for you
        // automatically.  Note also that, although the function obviously
        // needs to return a promise, you don't need to set this explicitly
        // either - the onRequest framework automatically wraps
        // your return inside a "new Promise". 

            res.send(result); 

        } catch (error) {
            res.send({error: error })
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

All of this, you'll understand, is coded in your project's functions/index.js file and deployed from a terminal session using a firebase command (see Getting serious with Firebase V9 if this is unfamiliar territory). Console.log statements placed in your function's code will produce output that you can view in the Cloud's functions console. If you're wondering how you test all this, see Using the Firebase emulator for some ideas. Personally, I find the emulator rather cumbersome and prefer to use live endpoints. But if I were doing a lot of work with functions I'd probably change my views. All these Google tools look alarming on first acquaintance but quickly become old friends.

Anyway, here's how you might call this function from your webapp client. I use the await keyword here rather than .then code patterns - I think this makes the code easier to follow. The code below is expected to live inside some sort of async function to sanction the use of await.

// Here's the URL for your Cloud function. It takes the following form:
// https://<region>-<project id>.cloudfunctions/net/,function name>

const functionUrl = 'https://europe-west2-yourProject.cloudfunctions.net/selectViaOnRequestFunction';

// Create a search parameter object

const searchParams = {
    surname: "Airey",
    forename: "John",
}

    // Create the required options for your request - in this case a POST
    // method, a content-type header specifying a JSON input and a body
    // containing a character representation of your input data

const requestOptions = {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify(searchParams),

try {

    // Make the asynchronous HTTPS request and receive its response 
    // in a local variable

    const response = await fetch(functionUrl, requestOptions)

    // Handle the response from the function. Magically, it comes back as
    // an object

    // Strangely, the JSON payload of "response" is only available as an
    // asynchronous ".json" function property of response - read the W3C
    // Fetch Living Standard if you want to know why

    const responseJSON = await response.json();
    console.log({ responseJSON }); 

} catch (error) {
    // Handle any errors
    console.error('Error calling function:', error);
}
Enter fullscreen mode Exit fullscreen mode

The onCall function

While onRequest works pretty well, I'm sure you'll agree that it would be nice if it were a little simpler. As a Firebase developer, you'll be particularly annoyed to find that you can't use 'localhost' as the 'authorised site' for an onRequest call. Google themselves obviously agreed and so they've developed an onCall version of the onRequest function specifically for use by a Firebase webapp working into Cloud functions within its own domain. The onCall model rubs all the rough edges off the onRequest function.

Here's the code for a sample selectViaOnCallFunction onCall function:

exports.selectViaOnCallFunction = functions.region('europe-west2').https.onCall(async (data, context) => {

    console.log({ data }) // displays properties of original data input object - no need to JSON.parse it

    // That's it - you're in!. Because you're "sandboxed" within your
    // own project domain there's no need for a "pre-flight" check.
    // You'll find that even calls from your local host are accepted.
    // You'll also find that your input data is both transmitted and
    // received as an object - no "stringification" here

    // Wondering what the "context" calling parameter might be? This is
    // an optional field designed to supply a user's "auth" information.
    // Inspection of this then enables you to reject requests that
    // aren't logged in and to access information details such as
    // 'context.auth.email" etc

    // Once comfortably plugged into your function, Firestore
    // collections and Cloud storage buckets are now available.
    // Remember, however, that you are now operating in a Node.js
    // environment so the API interfaces are rather different - see
    // "Working with Cloud Functions" at
    // (https://dev.to/mjoycemilburn/getting-serious-with-firebase-v9-part-4-cloud-storage-uploading-files-3p7c)
    // for background.

    // When you're finally ready to return a response or error object
    // proceed as follows:   

    const response ={
        myResponseProperty:data.myInputProperty + " - yeehaa!"
    }

    try {
        return response; // no need to stringify a JSON response object
    } catch (error) {
        return { error: error}
    }

    // The return is automatically wrapped within a promise.

})
Enter fullscreen mode Exit fullscreen mode

Calling arrangements are similarly straightforward. Instead of a Fetch instruction, you now use a call on the Firebase APIs. There's no need now to "stringify" your input data object and, while the following example may look a bit complicated, that's only because I've included the initial Firebase configuration. You'll surely have this in your webapp already. I've highlighted the setup bits that are specific to onCall.


// Here's the initialisation

import { httpsCallable } from 'firebase/functions'; // onCall specific

const firebaseConfig = {
    apiKey: "AIzaSy .. obfuscated ... tYEvb4DRN8",
          .. etc ..
    appId: "1:716624 .. obfuscated ... 542bef254721"
};

const app = initializeApp(firebaseConfig);

const functions = getFunctions(app, 'europe-west2');

// That's the end of the project setup. Now declare your onCall function

const selectViaOnCallFunction = httpsCallable(functions, 'selectViaOnCallFunction'); // onCall specific

// Now here's how you might use your function in an HTML button.  In
// this instance I've not used the optional "context" parameter - the
// function's input is just a simple data object. 

    <button onClick={async () => {

         try {
            const data = {myInputProperty: "yip yip"} // create an input data object
            const responseJSON = await selectViaOnCallFunction(data) // call the onCall function and get its result
            const responseProperty = responseJSON.data.myResponseProperty // the .data method is synchronous
            console.log(response) // logs "yip yip - yeehaa"
            } catch (error)  {
                    console.log("Failure: error details are : " + error); //baa
            }

        } 
    }>Click here to test access to the selectViaOnCallFunction function</button>
Enter fullscreen mode Exit fullscreen mode

I make that a maximum of 11 onCall-specific instructions to set up, submit and log an onCall function. Neat!

Thanks for reading this - I hope you find it useful.

Top comments (0)