[REQUIRED] Environment info
firebase-tools: 15.19.1
Platform: macOS
[REQUIRED] Test case
A minimal Next.js app deployed through the Hosting web frameworks integration
("source" + "frameworksBackend" in firebase.json), where the generated
SSR function's Cloud Run service is missing the public invoker IAM binding
(allUsers with roles/run.invoker).
Note: the app needs at least one page using next/image (or some other
reason for a backend, like a dynamic route). A fully static app never gets an
SSR function created, so there is nothing to reproduce against. Image
optimization is the easiest trigger and also what surfaces the failure on
otherwise prerendered pages.
We hit this state in production: the IAM policy on our ssr<site> service
was completely empty (just an etag). We don't know exactly how it got that
way. The repro below sidesteps that question by
removing the binding manually, which puts the service in the same state.
// firebase.json
{
"hosting": {
"source": ".",
"frameworksBackend": { "region": "us-central1" }
}
}
[REQUIRED] Steps to reproduce
-
npx create-next-app@latest repro and add a page that renders a
statically imported image with next/image. This is required, otherwise
no SSR function is created and the deploy is fully static.
-
firebase init hosting (web frameworks), then deploy live with
firebase deploy --only hosting. Everything works.
-
Put the service in the broken state:
gcloud run services remove-iam-policy-binding ssr<site> \
--member=allUsers --role=roles/run.invoker --region us-central1
-
Deploy a preview channel:
firebase hosting:channel:deploy preview --only <site>
The command succeeds and prints the channel URL.
-
Open the channel URL and load a page with an image.
[REQUIRED] Expected behavior
The channel deploy should check that the rewrite target is publicly
invokable, and either re-assert the invoker binding (live frameworks deploys
do this when they redeploy the function) or fail loudly. Even a one-line
warning ("the ssr function is not publicly invokable, dynamic routes on this
channel will return 403") would save a lot of debugging time.
[REQUIRED] Actual behavior
The channel deploy reports success, but the channel is broken for all
dynamic content:
-
Prerendered pages load fine, since they come from the Hosting CDN.
-
Anything that has to invoke the SSR function, including /_next/image,
returns Google's generic 403 page ("Error: Forbidden. Your client does not
have permission to get URL ...") with nothing in the function logs. The
request is rejected by IAM before the container runs, so there is no
invocation to log. For example:
https://<site>--preview-<hash>.web.app/_next/image?url=%2F_next%2Fstatic%2Fmedia%2F1.479aff49.jpg&w=1080&q=75
-> 403 Forbidden
https://<site>--preview-<hash>.web.app/about
-> 200 (prerendered, served from CDN)
-
Meanwhile the live channel looks healthy the whole time, because the
popular _next/image URLs are already in the Hosting CDN cache. That makes
this look like a preview-channel-specific problem and makes it very hard to
search for.
What finally surfaced it:
$ gcloud run services get-iam-policy ssr<site> --region us-central1
etag: ACAB
That's an empty policy, no allUsers binding at all. Restoring it fixed every
channel immediately, no redeploy needed:
gcloud run services add-iam-policy-binding ssr<site> \
--member=allUsers --role=roles/run.invoker --region us-central1
Running the channel deploy with --debug shows no IAM-related warning or
error anywhere, which is the core of the issue. Happy to attach the full
debug logs if that helps.
[REQUIRED] Environment info
firebase-tools: 15.19.1
Platform: macOS
[REQUIRED] Test case
A minimal Next.js app deployed through the Hosting web frameworks integration
(
"source"+"frameworksBackend"infirebase.json), where the generatedSSR function's Cloud Run service is missing the public invoker IAM binding
(
allUserswithroles/run.invoker).Note: the app needs at least one page using
next/image(or some otherreason for a backend, like a dynamic route). A fully static app never gets an
SSR function created, so there is nothing to reproduce against. Image
optimization is the easiest trigger and also what surfaces the failure on
otherwise prerendered pages.
We hit this state in production: the IAM policy on our
ssr<site>servicewas completely empty (just an etag). We don't know exactly how it got that
way. The repro below sidesteps that question by
removing the binding manually, which puts the service in the same state.
[REQUIRED] Steps to reproduce
npx create-next-app@latest reproand add a page that renders astatically imported image with
next/image. This is required, otherwiseno SSR function is created and the deploy is fully static.
firebase init hosting(web frameworks), then deploy live withfirebase deploy --only hosting. Everything works.Put the service in the broken state:
Deploy a preview channel:
The command succeeds and prints the channel URL.
Open the channel URL and load a page with an image.
[REQUIRED] Expected behavior
The channel deploy should check that the rewrite target is publicly
invokable, and either re-assert the invoker binding (live frameworks deploys
do this when they redeploy the function) or fail loudly. Even a one-line
warning ("the ssr function is not publicly invokable, dynamic routes on this
channel will return 403") would save a lot of debugging time.
[REQUIRED] Actual behavior
The channel deploy reports success, but the channel is broken for all
dynamic content:
Prerendered pages load fine, since they come from the Hosting CDN.
Anything that has to invoke the SSR function, including
/_next/image,returns Google's generic 403 page ("Error: Forbidden. Your client does not
have permission to get URL ...") with nothing in the function logs. The
request is rejected by IAM before the container runs, so there is no
invocation to log. For example:
Meanwhile the live channel looks healthy the whole time, because the
popular
_next/imageURLs are already in the Hosting CDN cache. That makesthis look like a preview-channel-specific problem and makes it very hard to
search for.
What finally surfaced it:
That's an empty policy, no allUsers binding at all. Restoring it fixed every
channel immediately, no redeploy needed:
Running the channel deploy with
--debugshows no IAM-related warning orerror anywhere, which is the core of the issue. Happy to attach the full
debug logs if that helps.