How to version firebase callable cloud functions endpoints? - firebase

Firebase offers HTTPS Callable function in Cloud Functions, which are similar but not identical to HTTP functions.
With http function, one is used to do API endpoint versioning by either adding the version string to the endpoint URL (e.g. /api/v1/customers/3) or by including the version in custom MIME types in the Header information.
Question: What is the right approach to version callable functions in firebase to make sure clients that still rely on the old payload structure will not to break once endpoints with parameter changes are deployed?

AFAIK there is no recommendation on this point in the Cloud Functions for Firebase documentation.
With Callable Cloud Functions, you can mimic the two approaches you describe in your question for API endpoint versioning.
"Adding the version string to the endpoint URL"
You can very well have several Callable Cloud Functions with a version number in their name, e.g.:
exports.doSomethingInThebackEndv1 = functions.https.onCall((data, context) => {
// ...
});
exports.doSomethingInThebackEndv2 = functions.https.onCall((data, context) => {
// ...
});
"Including the version in custom MIME types in the Header information"
You can add the version to the object you pass to the function when calling it from your front-end. For example with the JS SDK:
var doSomethingInThebackEnd = firebase.functions().httpsCallable('doSomethingInThebackEnd');
doSomethingInThebackEnd({ foo: 'bar', version: 1 })
.then((result) => {...});
Then in the back-end:
exports.doSomethingInThebackEnd = functions.https.onCall((data, context) => {
const version = data.version;
// do different things depending on the version value
});
I admit that it is not an out-of-the-box scalable solution and that it may request a lot of manual operations in case of many different versions...

Related

Firebase emulator: see outgoing HTTP traffic

I have a Cloud Function that calls to Chargebee. In index.ts:
const chargeBee = new ChargeBee();
...
chargeBee.configure({
site,
api_key: apiKey
});
...
export const finalizeSignup = https.onCall(
async (info: SignupInfo, ctx: CallableContext) => {
const cbCmd = chargeBee.hosted_page.retrieve(info.cbHostedPage);
const callbackResolver = new Promise<any>((resolve, reject) => {
// cbCmd.request returns a Promise that seems to do nothing.
// The callback works, however.
// Resolve/reject the Promise with the callback.
void cbCmd.request((err: any, res: any) => {
if (err) {
reject(err);
}
resolve(res);
});
});
// Calling Promise.resolve subscribes to the Promise.
return Promise.resolve(callbackResolver);
}
);
I am testing this function using the Firebase emulators, started via firebase emulators:start --only functions. Chargebee is responding strangely. They require the domain of their incoming requests to be whitelisted: my first guess is that the domain being used by my locally emulated Cloud Function is not whitelisted on the Chargebee side.
How do I see outgoing HTTP information sent by my locally emulated Cloud Function?
The connection is actually HTTPS, not HTTP.
The emulators provide no functionality to intercept network traffic of any form.
For HTTP: you have to apply your own tooling to monitor the HTTP traffic (ie Wireshark).
For HTTPS: possible to monitor using Wireshark, but impossible to analyze without knowing the SSL key. And in the setup above, where a third-party library is handling the request, there is currently no way to obtain the SSL key. I entered a feature request with Firebase to gauge the interest of developing a way to define an SSL key log when starting the Functions emulator, similar to Chrome. A user only identifying themselves as 'Oscar' told me in a private email that "I've already filed a feature regarding this topic to our engineering team regarding this matter, which will be discussed internally." So that tells us that (1) Firebase is aware that the feature is currently lacking, and (2) there is no progress to report on the feature.

Firebase functions route to specific http method

I want to have a separate function for each HTTP method (GET, POST, PATCH....) - for the same URI path, for example:
// express app
...
getUser.get('/api/v1/user/:id', async (req, res) => {
...
updateUser.patch('/api/v1/user/:id', async (req, res) => {
...
exports.getUser = functions.https.onRequest(getUser);
exports.updateUser = functions.https.onRequest(updateUser);
But I don't know how to specify hosting rewrites configuration for such cases.
Is it possible to route different HTTP methods to different functions (in firebase.json file)?
According to the documentation, Firebase Hosting doesn't you specify a method for rewriting. You can only provide a URI path.
What you should probably do here is create a single express app that contains all of the methods for the single endpoint, and export that through a single named function. Express will know what to do with the method.

Firebase (with react native) HTTPS function doesn't receive parameters

I'm using React native with Firebase. I deployed a function (without query params) and I'm able to call it without problems from my iPhone. When I add parameters, I'm able to run it only on my browser, but on my phone the parameters are undefined.
Firebase function
exports.testFunction = functions.https.onRequest((request, response) => {
const searchQuery = request.query.search;
response.status(200).send({data:searchQuery});
});
Client App
const testFunction = functions.httpsCallable('testFunction');
testFunction({search: "anything"})
I'm suspecting that this is a bug in either Firebase SDK, React Native's translation to iOS or hopefully a problem in my code, what could be the problem?
To call firebase functions from app, do not use onRequest; use onCall. See the doc for detailed implementation on both server and client side.
This is also mentioned in the doc of react-native-firebase, but the use of onCall is not emphasized (I think it should be highlighted to avoid confusion).
That said, it is possible to call an onRequest function from app, but the parameters are not under request.query, but under request.body.data. For example, in OP's scenario, the firebase function would be
exports.testFunction = functions.https.onRequest((request, response) => {
const searchQuery = request.body.data.search; // param is not in query
response.status(200).send({data:searchQuery});
});
However, calling onRequest from app is not recommended, because the success or error message sent back to the client cannot be properly handled by react-native-firebase. Thus, the best practice is to use onCall.

How to use httpsCallable on a region other then us-central1 for web

I have a deployed a cloud function which looks like this:
export const publishVersion = functions
.region("europe-west2")
.https.onCall(async (data, context) => {}
Then in my web client I am defining a function to call it like this:
import { functions } from "firebase";
const callPublishVersion = functions().httpsCallable("publishVersion");
export function publishVersion(
guidelineId: string,
guidelineReference: string
) {
return callPublishVersion({
id: guidelineId,
reference: guidelineReference
});
}
Note that the only reason I wrap the function is to have strict typing in my client, but that's besides the point of this question.
The problem is, if I run the web client on a local server (localhost) and call the function, two things seem to go wrong. I get a CORS error:
Access to fetch at 'https://us-central1-my-project.cloudfunctions.net/publishVersion' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.
And also it looks like it is trying to communicate with us-central1 even though my functions are deployed to europe-west2. When I deploy firebase-tools tells me what the url should be:
✔ functions[publishVersion(europe-west2)]: Successful create operation.
Function URL (publishVersion): https://europe-west2-baymard-gemini-dev.cloudfunctions.net/publishVersion
Where/how do I control things to prevent this CORS error and how can I direct httpsCallable to a different region?
The official documentation does not mention any of this.
--- edit ---
I just found out that if I deploy the function to us-central1 it works without errors. So the CORS part is not an issue (I'll remove it from the title) and the question becomes: How do I configure the web client to call the right region?
--- edit ---
I noticed this in the docs:
Note: To call a function running in any location other than the default us-central1, you must set the appropriate value at initialization. For example, on Android you would initialize with getInstance(FirebaseApp app, String region).
Which seems to be a good pointer, but I don't see how to configure this for web. So I'll narrow it down in the title too.
Found it by inspecting the source code. So this is not in the docs, and it was a bit confusing because of all the different ways you can get a handle to the functions instance, but this is the way:
const app = firebase.app();
const functions = app.functions("europe-west2");
const callPublishVersion = functions.httpsCallable("publishVersion");
here is a different version of this, uses the Modular Web-Version 9
import { initializeApp } from 'firebase/app';
import { getFunctions, httpsCallable } from "firebase/functions";
const app = initializeApp({
// Auth stuff
});
// add the location string as you call getFunctions
const functions = getFunctions(app, "europe-west3");
const myFunction = httpsCallable(functions, "myFunction");
The documentation just explains what Thijs found in the source. Extract at the time of writing:
To set regions on the client, specify the desired region at initialization:
var functions = firebase.app().functions('us-central1');
Certainly not easy to find, so an extra pointer here.

Use Firebase onRequest() or Express app.use() for the Slack API

Goal
Use the #slack/interactive-message package with firebase-functions to listen and respond to Slack messages and dialogs.
Question
I'm not sure how to use the #slack/interactive-message listener with firebase.
1) Do I use Firebase's functions.https.onRequest(), and somehow pass the req from Slack to slackInteractions.action()?
OR
2) Do I use app.use("/app", slackInteractions.expressMiddleware()); If so, where do slackInteractions.action()s go?
OR
3) Something else?
Code
// Express
import express = require("express");
const app = express();
const cors = require("cors")({
origin: "*"
});
app.use("*", cors);
// Firebase Functions SDK
import functions = require("firebase-functions");
const slackbotConfig = functions.config().slackbot;
const { createMessageAdapter } = require("#slack/interactive-messages");
const slackInteractions = createMessageAdapter(slackbotConfig.signing_secret);
app.use("/app", slackInteractions.expressMiddleware());
// Express route
app.post("/go", (req, res) => {
console.log("Hello from Express!");
res
.status(200)
.send("Hello from Express!")
.end();
});
exports.app = functions.https.onRequest(app);
exports.helloWorld = functions.https.onRequest((_req, res) => {
console.log("Hello from Firebase!");
res
.status(200)
.send("Hello from Firebase!")
.end();
});
tl;dr
I'm new to the details of Express and using middleware. Examples of the #slack/interactive-message show...
slackInteractions.start(port).then(() => {
console.log(`server listening on port ${port}`);
});
...and with Firebase Cloud Functions, this bit isn't relevant. I'm not sure how listeners, requests, and responses are integrated between Firebase and #slack/interactive-message
creator of #slack/interactive-messages here 👋
In short, your solution number 2 seems correct to me. While I don't have experience with Firebase functions, I have a pretty good understanding of express, and I'll provide some more details.
What is express middleware?
Express middleware is a name for a kind of function that processes an incoming HTTP request. All middleware functions can, on a request-by-request basis, choose to pre-process a request (usually by adding a property to the req argument), respond to the request, or post-process a request (like calculate the timing between the request and the response). It can do any one or combination of those things, depending on what its trying to accomplish. An express app manages a stack of middleware. You can think of this as a list of steps a request might work through before a response is ready. Each step in that list can decide to offer the response so that the next step isn't even reached for that request.
The cors value in your code example is a middleware function. It applies some rules about which origins your Firebase function should accept requests from. It applies those rules to incoming requests, and when the origin is not allowed, it will respond right away with an error. Otherwise, it allows the request to be handled by the next middleware in the stack.
There's another middleware in your example, and that's a router. A router is just a kind of middleware that knows how to split an app up into separate handlers based on the path (part of the URL) in the incoming request. Every express app comes with a built in router, and you attached a handler to it using the app.post("/go", () => {}); line of code in your example. Routers are typically the last middleware in the stack. They do have a special feature that people often don't realize. What are these handlers for routes? They are just more middleware functions. So overall, you can think of routers as a type of middleware that helps you divide application behavior based on the path of a request.
What does this mean for slackInteractions?
You can think of the slackInteractions object in your code as a router that always handles the request - it never passes the request onto the next middleware in the stack. The key difference is that instead of dividing application behavior by the path of the request, it divides the behavior using the various properties of a Slack interaction. You describe which properties exactly you care about by passing in constraints to the .action() method. The only significant difference between a typical router and slackInteractions, is that the value itself is not the express middleware, you produce an express middleware by calling the .expressMiddleware() method. It's split up like this so that it can also work outside of an express app (that's when you might use the .start() method).
Putting it together
Like I said, I don't have experience with Firebase functions specifically, but here is what I believe you should start with as a minimum for a function that only handles Slack interactions.
// Firebase Functions SDK
import functions = require("firebase-functions");
const slackbotConfig = functions.config().slackbot;
// Slack Interactive Messages Adapter
const { createMessageAdapter } = require("#slack/interactive-messages");
const slackInteractions = createMessageAdapter(slackbotConfig.signing_secret);
// Action handlers
slackInteractions.action('welcome_agree_button', (payload, respond) => {
// `payload` is an object that describes the interaction
console.log(`The user ${payload.user.name} in team ${payload.team.domain} pressed a button`);
// Your app does some asynchronous work using information in the payload
setTimeout(() => {
respond({ text: 'Thanks for accepting the code of conduct for our workspace' });
}, 0)
// Before the work completes, return a message object that is the same as the original but with
// the interactive elements removed.
const reply = payload.original_message;
delete reply.attachments[0].actions;
return reply;
});
// Express
import express = require("express");
const app = express();
app.use("/", slackInteractions.expressMiddleware());
exports.slackActions = functions.https.onRequest(app);

Resources