27

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.

Aditya Aggarwal
  • 416
  • 1
  • 5
  • 10
  • 1
    Can you edit your question to briefly show the Cloud Functions side of your express app and https function export? I need to see how you're setting things up. – Doug Stevenson Jul 06 '17 at 22:11
  • @DougStevenson done sir, Thank you for your reply. I'm fairly inexperienced with a JS based backend, so I've kept most of it as shown in the firebase-examples. – Aditya Aggarwal Jul 06 '17 at 22:18
  • 2
    Were you ever able to find a solution for this? I've also been struggling all day trying to solve this... – Pkmmte Jul 20 '17 at 00:09
  • @Pkmmte Not exactly a solution, a workaround. Please look at edit 2 for details. – Aditya Aggarwal Jul 20 '17 at 11:33
  • @AdityaAggarwal I took a slightly different approach to resolving this issue, which basically involves using a `main` function wrapper. See my answer below for more details. :) – Pkmmte Jul 20 '17 at 20:06

5 Answers5

45

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);
Pkmmte
  • 2,822
  • 1
  • 31
  • 41
  • In my case, I redirect everything to the `main` function so I never have to touch firebase.json ever again: `"source": "**"` – Pkmmte Jul 20 '17 at 20:08
  • 4
    @Pkmmte I don't understand why { "source": "/api", "function": "api" } is causing repeating /api/api in the url – Muhammad Hassan Nasr Aug 09 '17 at 00:19
  • 3
    @MuhammadHassan That's the way Firebase works internally for some reason. I don't agree with it either. It says "Hey, you wanna redirect `/api` to your `api` function? Alright, I'll pass it there as `api + /api`". Maybe they did this so you can redirect something like `/cat` to a function called `animals` like so: `animals/cat`. – Pkmmte Aug 09 '17 at 01:02
  • 3
    This has saved me a lot of time and money trying to host my api on app engine. Many thanks! – DevShadow Mar 06 '18 at 03:12
  • does this /api rewrite still happen - seems totally nonsensical? – triple Jun 26 '18 at 02:13
  • @triple As far as I'm aware, yeah. At least in my projects. – Pkmmte Jun 26 '18 at 02:47
  • 1
    Thanks, this works great when deployed, however locally I still have to hit the endpoint with the repeated `api` for it to work. How can I make it work both locally and deployed? – inorganik Oct 05 '18 at 03:48
  • @inorganik set [GOOGLE_APPLICATION_CREDENTIALS](https://firebase.google.com/docs/functions/local-emulator) – galki Nov 13 '18 at 10:59
  • Not working with Angular Single page application. Everything is being redirected to https://project-id.firebaseapp.com – Kartik Watwani Jan 09 '19 at 05:08
  • @Pkmmte i have deployment issue on vuejs/node js in firebase deploy. can you check this question https://stackoverflow.com/questions/56523054/how-to-deploy-vuejs-nodejs-app-in-firebase-hosting may i know whether i structured correctly and calling right firebase function. – PvDev Jun 18 '19 at 05:32
  • 2
    `"source": "/api/**"` will catch requests made to `/api/xyz` and `/api/` but not to `/api`. To make it inclusive, use `"source": "/api{,/**}"`. Source: https://firebase.google.com/docs/hosting/full-config#rewrites – secretshardul Jan 24 '21 at 07:35
  • @Pkmmte why it does not work with me ? i tried same way as your https://stackoverflow.com/questions/67931862/rewrite-does-request-to-function-when-proper-path-provided – Ashish Jun 11 '21 at 06:37
  • 1
    Many years later, I didn't find this Question and Answer after 2 days of fight with this issue. Only when drafting my own question - this one popped up and I finally solved. Thank you sir! And someone likes FromSoftware games for their hardcore... Please :) – Volodymyr RT Apr 03 '22 at 05:57
28

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);
    
Sam
  • 291
  • 4
  • 9
  • 1
    This had me confused for ages. Thank you! – optilude May 10 '20 at 21:53
  • 1
    Works great thank you! I think this is a better solution than the second express server the accepted answer gives but both solve the problem – dinkydani Jun 09 '20 at 05:52
  • 2
    This actually worked. Firebase should be ashamed of their unintuitive documentation and need for making gimmicks like this to make it work the same way locally and on the production. Thanks! – jean d'arme Feb 03 '21 at 00:07
0

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;
};
njho
  • 2,023
  • 4
  • 22
  • 37
0

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"
}
Gavin Chebor
  • 265
  • 3
  • 5
-1

This works for me.

In firebase.json file:

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

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

  • I tried this and it works. I have a secondary website for api subdomain. `api.mycustomdomain` ` "rewrites": [ { "source": "**", "function": "/" } ] ` `api.mycustomdomain/function-name` runs just like `https://us-central1-project-name.cloudfunctions.net/function-name` – fthdgn Jan 06 '22 at 12:54