Firebase functions take MINUTES to write in Firestore

All we need is an easy explanation of the problem, so here it is.

I’m building a mobile app and on some actions (like userCreate), I trigger some Firebase functions to perform an API call to a third service, and then write something in the Firestore database.

Everything works in theory, but in practice, the API calls are quite fast (even with cold start scenarios), but the database writes can take several MINUTES to complete (if they do at all, I suspect that sometimes it takes too long and times out).

Since the API call work just fine, and so does the DB write on some occasion, I suspect that this is simply due to very poor async management from me since I know about nothing in JS.

Here are two of many example functions which are concerned by this issue, just to showcase that it happens whereas I’m triggering functions onCreate or on HTTPS.

onCreate function

const functions = require("firebase-functions");
const axios = require('axios')

// The Firebase Admin SDK to access the Firestore.
const admin = require('firebase-admin');
admin.initializeApp();

// Third party service credentials generation during onCreate request
exports.

buildCredentials = functions.auth.user().onCreate((user) => {


// Request to Third party to generate an Client Access Token
axios({
    method: "post",
    url: "https://api.ThirdParty.com/api/v1/oauth/token",
    data: "client_id=xxx&client_secret=yyy&grant_type=client_credentials&scope=all:all",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
  })
    .then(function (response) {
      //handle success
      console.log('new user created: ')
      console.log(user.id);
      console.log(response.data);
    
      // We write a new document in the users collection with the user ID and the Client Access Token
      const db = admin.firestore();
      const newUser = {
        uid: user.uid,
        clientAccessToken: response.data.access_token
      };

      db.collection('users').doc(user.uid).set(newUser)
      .catch(function (error) {
      //handle error
      console.log(error);
    });

    })
    .catch(function (response) {
      //handle error
      console.log(response);
    });
})  

HTTPS onCall function

exports.paymentRequest = functions.https.onCall(async (data, context) => {
    const clientAccessToken = data.clientAccessToken;
    const recipientIban = data.recipientIban;
    const recipientName = data.recipientName;
    const paymentDescription = data.paymentDescription;
    const paymentReference = data.paymentReference;
    const productPrice = parseInt(data.productPrice);
    const uid = data.uid;

    const jsonData = {
        "destinations": [
                {
                    "accountNumber": recipientIban,
                    "type": "iban"          
                }
            ],
            "amount": productPrice,
            "currency": "EUR",
            "market": "FR",
            "recipientName": recipientName,
            "sourceMessage": paymentDescription,
            "remittanceInformation": {
                "type": "UNSTRUCTURED",
                "value": paymentReference
                },
            "paymentScheme": "SEPA_INSTANT_CREDIT_TRANSFER"
    };
    (async function(){
    response = await axios({
        method: "post",
        url: "https://api.ThirdParty.com/api/v1/pay",
        headers: { 
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Bearer ${clientAccessToken}`,},
        data: jsonData,
        })

        // We write a the payment request ID in the user's document
        const db = admin.firestore();
        const paymentRequestID = response.data.id;

        db.collection('users').doc(uid).set({
            paymentRequestID: paymentRequestID
        }, { merge: true })
        .catch(function (error) {
            //handle error
            console.log(error);
            });
        console.log(response.data)
        return response.data
    })()
})

Am I on the right track thinking that this is an async problem?
Or is it a Firebase/Firestore issue?

Thanks

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

You are not returning the promises returned by the asynchronous methods (axios() and set()), potentially generating some "erratic" behavior of the Cloud Function.

As you will see in the three videos about "JavaScript Promises" from the official Firebase video series you MUST return a Promise or a value in a background triggered Cloud Function, to indicate to the platform that it has completed, and to avoid it is terminated before the asynchronous operations are done or it continues running after the work has been completed.

The following adaptations should do the trick (untested):

onCreate Function:

buildCredentials = functions.auth.user().onCreate((user) => {

    // Request to Third party to generate an Client Access Token
    return axios({
        method: "post",
        url: "https://api.ThirdParty.com/api/v1/oauth/token",
        data: "client_id=xxx&client_secret=yyy&grant_type=client_credentials&scope=all:all",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
      })
        .then(function (response) {
          //handle success
          console.log('new user created: ')
          console.log(user.id);
          console.log(response.data);
        
          // We write a new document in the users collection with the user ID and the Client Access Token
          const db = admin.firestore();
          const newUser = {
            uid: user.uid,
            clientAccessToken: response.data.access_token
          };
    
          return db.collection('users').doc(user.uid).set(newUser)
    
        })
        .catch(function (response) {
          //handle error
          console.log(response);
          return null;
        });
    })  

Callable Function:

exports.paymentRequest = functions.https.onCall(async (data, context) => {
    
    try {
        
        const clientAccessToken = data.clientAccessToken;
        const recipientIban = data.recipientIban;
        const recipientName = data.recipientName;
        const paymentDescription = data.paymentDescription;
        const paymentReference = data.paymentReference;
        const productPrice = parseInt(data.productPrice);
        const uid = data.uid;
    
        const jsonData = {
            // ...
        };
        const response = await axios({
            method: "post",
            url: "https://api.ThirdParty.com/api/v1/pay",
            headers: {
                'Content-Type': 'application/json',
                Accept: 'application/json',
                Authorization: `Bearer ${clientAccessToken}`,
            },
            data: jsonData,
        })
    
        // We write a the payment request ID in the user's document
        const db = admin.firestore();
        const paymentRequestID = response.data.id;
    
        await db.collection('users').doc(uid).set({
            paymentRequestID: paymentRequestID
        }, { merge: true })
            
        console.log(response.data)
        return response.data
        
    } catch (error) {
        
        // See https://firebase.google.com/docs/functions/callable#handle_errors    
    
    }
    
})

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

Leave a Reply