Skip to content

Cloud Functions

Created: 2020-03-26 16:36:48 -0700 Modified: 2020-05-23 12:48:05 -0700

  • There are a few different ways to run functions
    • Calling them directly: this is basically just like making an asynchronous function call right from JavaScript, but the function happens to reside in the cloud. E.g.
const addMessage = firebase.functions().httpsCallable('addMessage');
const result = await addMessage({text: messageText});
const sanitizedMessage = result.data.text;

(just make sure to wrap this in a try/catch and handle errors - here are just the error codes themselves)

  • Also note: errors can include extra data:
throw new functions.https.HttpsError('failed-precondition', 'Missing FOO');
  • Note: if you want, you can specify a timeout: const fooFn = firebase.functions().httpsCallable(‘foo’, { timeout: 8000 });

  • Also, it’s just JSON being passed, so numbers remain as numbers, booleans remain as booleans, etc.

  • This automatically handles authentication information.

  • These are just HTTP requests in the end, so there’s a protocol you can follow (reference). I suppose this would let you specify cookies or something, although you probably can’t get them on the back-end unless Firebase exposes them (which they probably don’t and you should just use HTTP functions if you want cookies).

  • Calling them as though they’re REST APIs: this is just like you set up an Express or Restify server and wanted to GET/POST/PATCH/etc. to the APIs. You get Express’s (req, res) combo (reference).

    • If you want Firebase authentication information in these requests, look at this.
  • You can schedule them to run every so often

  • You have to export all of your functions from index.js for them to be visible. Despite that they all need to be exported there, you can define them wherever you want (e.g. multiple files and then import/export in index.js), and you can/should deploy them by choosing specific functions (reference).

  • Each function has its own logs. These show public IP addresses, so don’t show them on-stream. Access them via the console → Functions → Logs. “console.log” output will go here, e.g.

  • Middleware can be used for HTTP functions (reference)
    • Middleware is handled through express (“yarn why express” from your “functions” folder shows this)
    • Chaining middleware is ugly without a helper function:
return cookieParser()(req, res, () =>
cors(req, res, () => yourNonMiddlewareHandlerFunction(req, res))
);
  • Debugging functions locally with the emulator is dead simple:
    • Start the emulators with “—inspect-functions”: “firebase emulators:start —inspect-functions —only firestore,functions”
    • Open any Chrome window, press F12, and click the gem thing:

  • Tips and tricks (keyword: optimizations) (reference)
    • The execution environment of a function may be recycled, meaning any global-scope variables would be reused. Because of this, they suggest placing any reusable variables in the global scope (reference). They also suggest lazily initializing them so that code-paths that don’t use the variables won’t do the initialization at all until needed (reference).
    • NodeJS lets you require the same module multiple times without running it multiple times (to test this, just put “console.log(‘hello’);” into a module and require it many times). This means that you should put your module imports as close to the code using them as possible (reference).

There is no way to get access to proper environment variables. Instead, you use Firebase’s configuration (reference). This is pretty simple: you just set, unset, or get keys:

  • Get:
    • firebase functions:config:get
  • Set:
    • firebase functions:config:set foo.bar=“baz” foo.qux=“quack”
    • After setting any config values, you must redeploy functions that use them.
  • Access from the Function itself:
    • functions.config().someservice.id

Note that the keys all have to be lowercase, and I think they require two parts, e.g. “first_part.second_part=foo”.

If you want newlines in the value that you’re setting, you have to encode them yourself (reference). For example, when I was specifying a crypto key, I knew that they were base64-encoded, so that meant “n” couldn’t naturally appear, so when I specified the key on the command line, I typed “n” instead of newlines. In the function itself, I replaced all “\n” with “n”.

If you want a way of managing these configuration values through a file, you can look at this blog post. The summary is that they unset every “env” variable, then they pipe a JSON file into the “set” command for all of those variables.

”Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions” (reference)

Section titled ”Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions” (reference)

This apparently shows up just for the sake of upselling; it doesn’t indicate that you’re trying to do something that isn’t covered by the Spark plan.

CORS despite being a callable function (as opposed to an HTTP function)

Section titled CORS despite being a callable function (as opposed to an HTTP function)

The error looks something like this:

Access to fetch at ‘https://us-central1-adamlearns-dev.cloudfunctions.net/createAccount’ 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.

This problem is caused by something that isn’t CORS, but it’s tough to narrow it down. Below are some of my issues when I hit this, but there are instances of this being hit online when people misconfigure error codes, throw from a catch, or a million other things.

See this code:

createAccount.js
exports.default = functions.https.onCall(async (data, context) => {
// ...
})
// index.js
exports.createAccount = require('./createAccount');

This produced a function called “createAccount-default”, not “createAccount”, so I got an error trying to call “createAccount”. The fix in my case was to add “.default” to index.js like this:

exports.createAccount = require('./createAccount').default;

I was in my “firebase use prod” environment when I wanted to be in “firebase use dev”.

  • Are you sure you actually have a function by that name?
    • When this has happened to me, I forgot to export the function from my index.js.

A Promise rejects with deadline-exceeded despite that nothing exceeded a deadline

Section titled A Promise rejects with deadline-exceeded despite that nothing exceeded a deadline

Note: this is likely just a transient bug that no one will encounter after long enough (it’s April 1st, 2020 as I write this).

This is caused by a bunch of stuff:

  1. This code starts a Promise.race (reference):
const response = await Promise.race([
this.postJSON(url, body, headers),
failAfter(timeout),
this.cancelAllRequests
]);
  1. Even if postJSON is successful, failAfter will always reject (reference).
  2. Chrome breaks on Promise rejections unless you turn off “Pause on exceptions”

This means that if you’re in Chrome, it’ll break on an “error” with deadline-exceeded even though nothing went wrong. I.e. there’s no error and this bug is as close as we’ll probably get to canceling the failAfter timer.

E.g. you go to your “Detailed usage stats”

…and see this:

As the picture shows, the most expensive calls are ~7-8 seconds even though it can run in several milliseconds most of the time. This is potentially indicative of several things:

  1. Your function had a cold start. The time this takes is almost entirely out of your control (reference), but this shouldn’t happen once the function is getting hit frequently enough in production.
  2. You wrote your function poorly. Read their tips and tricks for how to optimize functions.
  3. You have an outright bug in your function. If you’re seeing execution times of extremely high values like 25s, 30s, or 60s, then you probably forgot to return a promise somewhere.