Aditya Aggarwal
Aditya Aggarwal

Reputation: 416

Firebase Hosting with dynamic cloud functions rewrites

I have an express.js based cloud functions app on firebase in a function named api. To use a custom domain, I'm trying to use Firebase Hosting rewrites to route the specific URL to the function. I'm following the official documentation about cloud functions and Firebase hosting here, https://firebase.google.com/docs/hosting/functions, and have tried many combinations including the following:

"rewrites": [
      {
        "source": "/api/**",
        "function": "api"
      }
    ]

"rewrites": [
      {
        "source": "/api/:path1/:dat1/dat",
        "function": "api/:path1/:dat1/dat"
      }
    ]
"rewrites": [
      {
        "source": "/api/path1/dat1/dat",
        "function": "api"
      }
    ]
"rewrites": [
      {
        "source": "/api/*/*/*",
        "function": "api"
      }
    ]

Sadly, It doesn't seem to work for any possible combination. My express app has the following GET paths I plan on using:

'/api/users/:userId/:userData'
'/api/users/:userId/:userData/json'
'/api/users/:userId/'

and others similar to these. the :userId and :userData are the params in my request, as it works with express.js

The required functions work as expected in

https://my-firebase-app.cloudfunctions.net

but they do not work with

https://my-app.firebaseapp.com

Please tell me how these are supposed to work and what I'm doing wrong.

EDIT: Here is a sample of what my cloud functions export looks like

const functions = require('firebase-functions');
const express = require('express');
const app = express();

app.get('/users/:userId/:userData/json', (req, res) => {
    // Do App stuff here
}
// A couple more app.get in the same format

exports.api = functions.https.onRequest(app);

EDIT 2: After @DougStevenson's suggestion, I tried the following configuration

I tried the following in my firebase.json ,

{
  "hosting": {
    "rewrites": [
      {
        "source": "/api",
        "function": "api"
      }
    ],
    "public": "public"
  }
}

But I got the same problem, the function is never called. I read about how the rewrites are last resort options and if there are files present in the hosting, it will not go to the specified function.(I tried looking for the SO post where this ws mentioned but I can't find it) So I removed the 404.html and index.html files from hosting's public directory, since I don't need them anyway. But the problem still remained.

EDIT 2: Okay, so after a lot of trial and error, I just had to hard code the paths in the following format:

rewrites : [
      {
        "source": "/users/**/**/json",
        "function": "api"
      },
      {
        "source": "/api/users/**/**/json",
        "function": "api"
      }
]

After this, the express app is configured something like this:

app.get('/users/:userId/:userData/json', Foo)

I still would prefer if someone could suggest a better way to accomplish this rather than manually putting in each required Uri in the hosting rewrites.

Upvotes: 27

Views: 18340

Answers (6)

Tomer Omri
Tomer Omri

Reputation: 1091

For anyone setting up both local and production environments with Firebase, especially when connecting a custom domain, here's the correct setup as of October 2024.

I'm using Firebase Hosting & Functions, and for local development, I run everything with Firebase emulators.

Key Setup

In your firebase.json, you can configure the emulators like this:

{
  "emulators": {
    "functions": {
      "port": 5001
    },
    "hosting": {
      "port": 5003,
      "rewrites": [
        {
          "source": "/api/users/**",
          "function": "users"
        },
        {
          "source": "**",
          "destination": "/index.html"
        }
      ]
    }
  }
}

Important Notes: Local Hosting: Make sure your local hosting environment (env.development) points to the hosting emulator (on port 5003) and not the function port.

Example

SOME_ENV_SERVER=http://localhost:5003/api

Avoid pointing requests directly to the functions emulator, if you do so, you will need to maintain 2 different routes, one for dev and one for prod:

SOME_ENV_SERVER=http://localhost:5001/myapp-1234/us-central1

This setup ensures that API calls are routed correctly through your local hosting emulator, allowing seamless development both locally and in production.

Upvotes: 0

Ximo Mompó
Ximo Mompó

Reputation: 9

This works for me.

In firebase.json file:

"rewrites": [{
  "source": "/api/**",
  "function": "/"
}]

Make sure your functions are hosted in the us-central1 region

Upvotes: -2

Gavin Chebor
Gavin Chebor

Reputation: 275

Passing data directly to cloud function using Query params just works, with no custom express middleware.

Here is how the url would look like:

http://localhost:5000/api/?userId=yop&chart=blue

The cloud function:

export const api = functions.https.onRequest(async (req: any, res: any) => {

 const userId = req.query.userId
 const chartId = req.query.chart 

})

Rewrites remain at a minimum, because the url is still '/api' with Query params

"rewrites": [ {
  "source": "/api",
  "function": "api"
}

Upvotes: 0

njho
njho

Reputation: 2158

Another option, if you want both cloudFunction and hosted URLS to work is to check if the URL is coming from the hosted URL.

You'd use this function at any time you wanted the params.

export const splitParams = (req: any): string[] => {
  let params = req.params[0];
  const vals: string[] = [];

  // If params starts with a '/' remove it
  params = params.startsWith('/')
    ? params.substr(1, params.length - 1)
    : params;
  params = params.split('/');

  // For hosted URLs the parameters need to be shifted
  if (
    req.headers['x-forwarded-host'] === 'myURL' 
  ) {
    params.shift();
  }

  for (const param of params) {
    if (param !== '') {
      vals.push(param);
    }
  }

  return vals;
};

Upvotes: 0

Sam
Sam

Reputation: 331

You can use a single Firebase hosting rewrite rule with a complementing rewrite middleware in Express.

  1. Add a rewrite in your firebase.json file.

    {
      "source": "/api/**",
      "function": "api"
    }
    
  2. Include an app.use() middleware to rewrite the request url.

    const functions = require('firebase-functions');
    const express = require('express');
    
    const API_PREFIX = 'api';
    const app = express();
    
    // Rewrite Firebase hosting requests: /api/:path => /:path
    app.use((req, res, next) => {
        if (req.url.indexOf(`/${API_PREFIX}/`) === 0) {
            req.url = req.url.substring(API_PREFIX.length + 1);
        }
        next();
    });
    
    app.get('/users/:userId/:userData/json', (req, res) => {
        // Do App stuff here
    });
    
    exports[API_PREFIX] = functions.https.onRequest(app);
    

Upvotes: 32

Pkmmte
Pkmmte

Reputation: 2868

What seems to be the main issue is that this:

{
    "source": "/api",
    "function": "api"
}

is actually rewriting to https://my-firebase-app.cloudfunctions.net/api/api instead of https://my-firebase-app.cloudfunctions.net/api like you'd expect. Notice how api is repeated.

My solution to this is to create a main function which hosts all other top-level functions:

const functions = require('firebase-functions');
const express = require('express');
const app = express();

app.get('/users/:userId/:userData/json', (req, res) => {
    // Do App stuff here
}
// A couple more app.get in the same format

// Create "main" function to host all other top-level functions
const main = express();
main.use('/api', app);

exports.main = functions.https.onRequest(main);

You can now use this main function to delegate to all other functions without breaking your URL structure:

{
    "source": "/api/**", // "**" ensures we include paths such as "/api/users/:userId"
    "function": "main"
}

And voila! You can now access all api function calls via https://my-app.firebaseapp.com/api/users/:userId/:userData just like you'd expect.

Calling this endpoint, now rewrites to https://my-firebase-app.cloudfunctions.net/main/api which is technically correct. You can then add more top-level functions by simply adding them to your main function if you wish:

const hooks = express();
main.use('/hooks/, hooks);

Upvotes: 47

Related Questions