Code templates for Firebase onRequest and onCall Functions
Composition VIII - Wassily Kandinsky
Last reviewed: Dec 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:
- Heavyweight processing that would make your webapp look like a dog if you tried it in the browser
- Processing that uses security certificates (eg Paypal keys). Exposing these in a webapp would be disastrous
- 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 a remote page on the web with a URL along the lines of:
https://targetSite.com?param1="1"¶m2="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 conventional web 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 instructions in its "req" parameter and, in response, returns data 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 and user-based Firestore rules are
// bypassed here. 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'. (Note that if you actually
// /wanted/ your function to be publicly accessible, you could set
// the header to '*')
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 })
}
}
});
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);
}
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. If you're working entirely within your own domain, it's simply going to be a nuisance to have to work with all that "pre-flight" nonsense. 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.
})
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>
I make that a maximum of 11 onCall-specific instructions to set up, submit and log an onCall function. Neat!
You might also be interested to know that cloud functions are addressable directly with individual https
addresses. You'll see these listed in the output of the deploy command. They take the form:
https://us-central1-my-project.cloudfunctions.net/my-request-function
This is a wonderfully useful arrangement - for example, you might use a function's URL in the Cloud Scheduler to launch a backup task, or (via a curl
command) to send an alert message when a script triggers an exception. Have a word with chatGPT if you want more details or if you need to find out how to add input and output parameters.
Thanks for reading this - I hope you find it useful.
Top comments (0)