From c0fb2a41af5daff77bfa339c33095c593fd1b17f Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 20 Jul 2026 14:26:57 +0100 Subject: [PATCH 1/6] fix: improve error message when deploying to a gcp project where firebase not enabled --- CHANGELOG.md | 1 + src/functionsConfig.spec.ts | 48 +++++++++++++++++++++++++++++++++++++ src/functionsConfig.ts | 22 +++++++++++++---- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2816c4eeb..e8f4a4c6522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ +- Show a friendly error when deploying functions to a Google Cloud project that doesn't have Firebase enabled, instead of a raw 404 (#10379) - Add `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` HTTP headers to `OneMcpServer` requests per the MCP 0728 standard release candidate (https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ and https://modelcontextprotocol.io/seps/2243-http-standardization). - Fixes Storage Emulator to support JSON uploads larger than 100KB without hanging or throwing 413 error (#8355) diff --git a/src/functionsConfig.spec.ts b/src/functionsConfig.spec.ts index 158f4df6723..233407cd7f8 100644 --- a/src/functionsConfig.spec.ts +++ b/src/functionsConfig.spec.ts @@ -1,6 +1,10 @@ import { expect } from "chai"; +import nock from "./test/helpers/nock"; import * as functionsConfig from "./functionsConfig"; +import { FirebaseError } from "./error"; + +const FAKE_PROJECT_ID = "my-project"; describe("config.parseSetArgs", () => { it("should throw if a reserved namespace is used", () => { @@ -42,3 +46,47 @@ describe("config.parseUnsetArgs", () => { ]); }); }); + +describe("config.getFirebaseConfig", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + afterEach(() => { + expect(nock.isDone()).to.be.true; + }); + + it("should return the admin SDK config on success", async () => { + nock("https://firebase.googleapis.com") + .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) + .reply(200, { projectId: FAKE_PROJECT_ID }); + + const config = await functionsConfig.getFirebaseConfig({ project: FAKE_PROJECT_ID }); + + expect(config).to.deep.eq({ projectId: FAKE_PROJECT_ID }); + }); + + it("should throw a friendly error when the project doesn't have Firebase enabled", async () => { + nock("https://firebase.googleapis.com") + .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) + .reply(404, { error: { message: "Requested entity was not found." } }); + + await expect(functionsConfig.getFirebaseConfig({ project: FAKE_PROJECT_ID })) + .to.be.rejectedWith(FirebaseError, /doesn't have Firebase enabled/) + .and.eventually.have.property("status", 404); + }); + + it("should rethrow non-404 errors as-is", async () => { + nock("https://firebase.googleapis.com") + .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) + .reply(500, { error: { message: "Internal error" } }); + + await expect(functionsConfig.getFirebaseConfig({ project: FAKE_PROJECT_ID })) + .to.be.rejectedWith(FirebaseError, "Internal error") + .and.eventually.have.property("status", 500); + }); +}); diff --git a/src/functionsConfig.ts b/src/functionsConfig.ts index fe5634b92b5..cdbb795f6c9 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -1,7 +1,7 @@ import * as _ from "lodash"; import * as clc from "colorette"; -import { firebaseApiOrigin } from "./api"; +import { consoleOrigin, firebaseApiOrigin } from "./api"; import { Client } from "./apiv2"; import { ensure as ensureApiEnabled } from "./ensureApiEnabled"; import { FirebaseError } from "./error"; @@ -114,10 +114,22 @@ export function getAppEngineLocation(config: any): string { export async function getFirebaseConfig(options: any): Promise { const projectId = needProjectId(options); - const response = await apiClient.get( - `/v1beta1/projects/${projectId}/adminSdkConfig`, - ); - return response.body; + try { + const response = await apiClient.get( + `/v1beta1/projects/${projectId}/adminSdkConfig`, + ); + return response.body; + } catch (err: any) { + if (err instanceof FirebaseError && err.status === 404) { + throw new FirebaseError( + `Cannot deploy to project ${clc.bold(projectId)} because it doesn't have Firebase enabled. ` + + `Add Firebase to this Google Cloud project in the Firebase console, then try again:\n\n` + + `${consoleOrigin()}/project/${projectId}/settings/general`, + { original: err, exit: 1, status: 404 }, + ); + } + throw err; + } } // If you make changes to this function, run "node scripts/test-functions-config.js" From 4001b6df2995a03aa93bd75f79f92e34b90f3f0b Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 21 Jul 2026 15:43:14 +0100 Subject: [PATCH 2/6] Update src/functionsConfig.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/functionsConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functionsConfig.ts b/src/functionsConfig.ts index cdbb795f6c9..414c2b928bc 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -119,7 +119,7 @@ export async function getFirebaseConfig(options: any): Promise Date: Tue, 18 Aug 2026 11:28:34 +0100 Subject: [PATCH 3/6] fix: make the adminSdkConfig 404 message command-neutral getFirebaseConfig also backs functions:delete, functions:export and the ext:* commands, so wording the error as a deploy failure was wrong for most callers. The endpoint also 404s for missing, deleted and inaccessible projects, so state the project was not found and point at projects:addfirebase rather than asserting Firebase is not enabled and linking a console page that may not resolve. --- CHANGELOG.md | 2 +- src/functionsConfig.spec.ts | 21 ++++++++++++++------- src/functionsConfig.ts | 11 +++++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42492ca87d9..094222fe603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -- Show a friendly error when deploying functions to a Google Cloud project that doesn't have Firebase enabled, instead of a raw 404 (#10379) +- Replaced the raw 404 from `adminSdkConfig` with a message naming the project and pointing at `firebase projects:addfirebase` (#10379) - Configured OneMCP server tools to require a Firebase project by default, with options to opt-out specific tools (such as Developer Knowledge document search). - Fixed a bug where deploying functions with the `dartfunctions` experiment enabled could incorrectly prompt to delete existing GCF v2 functions. - Added `outputSchema` support for local MCP tools. diff --git a/src/functionsConfig.spec.ts b/src/functionsConfig.spec.ts index 233407cd7f8..19393114fe2 100644 --- a/src/functionsConfig.spec.ts +++ b/src/functionsConfig.spec.ts @@ -2,6 +2,7 @@ import { expect } from "chai"; import nock from "./test/helpers/nock"; import * as functionsConfig from "./functionsConfig"; +import { firebaseApiOrigin } from "./api"; import { FirebaseError } from "./error"; const FAKE_PROJECT_ID = "my-project"; @@ -61,7 +62,7 @@ describe("config.getFirebaseConfig", () => { }); it("should return the admin SDK config on success", async () => { - nock("https://firebase.googleapis.com") + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) .reply(200, { projectId: FAKE_PROJECT_ID }); @@ -70,18 +71,24 @@ describe("config.getFirebaseConfig", () => { expect(config).to.deep.eq({ projectId: FAKE_PROJECT_ID }); }); - it("should throw a friendly error when the project doesn't have Firebase enabled", async () => { - nock("https://firebase.googleapis.com") + it("should throw a friendly error on 404 that points at projects:addfirebase", async () => { + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) .reply(404, { error: { message: "Requested entity was not found." } }); - await expect(functionsConfig.getFirebaseConfig({ project: FAKE_PROJECT_ID })) - .to.be.rejectedWith(FirebaseError, /doesn't have Firebase enabled/) - .and.eventually.have.property("status", 404); + const err = await expect(functionsConfig.getFirebaseConfig({ project: FAKE_PROJECT_ID })).to.be + .rejected; + + expect(err).to.be.instanceOf(FirebaseError); + expect(err.status).to.eq(404); + expect(err.message).to.contain(`firebase projects:addfirebase ${FAKE_PROJECT_ID}`); + // getFirebaseConfig also backs functions:delete, functions:export and ext:*, + // so the message must not be phrased as a deploy failure. + expect(err.message).to.not.contain("deploy"); }); it("should rethrow non-404 errors as-is", async () => { - nock("https://firebase.googleapis.com") + nock(firebaseApiOrigin()) .get(`/v1beta1/projects/${FAKE_PROJECT_ID}/adminSdkConfig`) .reply(500, { error: { message: "Internal error" } }); diff --git a/src/functionsConfig.ts b/src/functionsConfig.ts index 414c2b928bc..fe9e58870df 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -1,7 +1,7 @@ import * as _ from "lodash"; import * as clc from "colorette"; -import { consoleOrigin, firebaseApiOrigin } from "./api"; +import { firebaseApiOrigin } from "./api"; import { Client } from "./apiv2"; import { ensure as ensureApiEnabled } from "./ensureApiEnabled"; import { FirebaseError } from "./error"; @@ -120,11 +120,14 @@ export async function getFirebaseConfig(options: any): Promise Date: Thu, 20 Aug 2026 14:39:18 +0100 Subject: [PATCH 4/6] chore: add trailing newline to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a370db7457e..830dd350833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Replaced the raw 404 from `adminSdkConfig` with a message naming the project and pointing at `firebase projects:addfirebase` (#10379) \ No newline at end of file +- Replaced the raw 404 from `adminSdkConfig` with a message naming the project and pointing at `firebase projects:addfirebase` (#10379) From c560aaf02a8bf558c613ecc6fb7ef17fbf6bf0a4 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 24 Aug 2026 16:09:10 +0100 Subject: [PATCH 5/6] refactor: move adminSdkConfig 404 message into a helper Keeps the multi-paragraph message as a template literal alongside the other messages in the file so its shape is visible, and rewords the changelog entry from the user's point of view. --- CHANGELOG.md | 2 +- src/functionsConfig.ts | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 830dd350833..70644ce20d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Replaced the raw 404 from `adminSdkConfig` with a message naming the project and pointing at `firebase projects:addfirebase` (#10379) +- Improved the error shown when deploying to a Google Cloud project that does not have Firebase enabled (#10379) diff --git a/src/functionsConfig.ts b/src/functionsConfig.ts index fe9e58870df..0996189a17c 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -39,6 +39,17 @@ To run this legacy command temporarily, run the following command and try again: firebase experiments:enable ${LEGACY_RUNTIME_CONFIG_EXPERIMENT} `; +// adminSdkConfig 404s for any project this account can't see, so the message +// must not assert a cause. The addfirebase hint covers the Cloud-project case. +const projectNotFoundMessage = (projectId: string): string => + `Firebase project ${clc.bold(projectId)} was not found. +Make sure the project exists and that your account has access to it. + +If ${clc.bold(projectId)} is a Google Cloud project without Firebase, add Firebase to it: + + firebase projects:addfirebase ${projectId} +`; + export function getFunctionsConfigDeprecationMessage(): string { return FUNCTIONS_CONFIG_DEPRECATION_MESSAGE; } @@ -120,16 +131,12 @@ export async function getFirebaseConfig(options: any): Promise Date: Thu, 3 Sep 2026 13:06:18 +0100 Subject: [PATCH 6/6] test: clear nock interceptors between getFirebaseConfig tests The suite only asserted nock.isDone(), so a test that failed before its interceptor was used left it registered on the module-level mock agent, where it carries over to later spec files in the same mocha run. Clean after reading isDone, so the assertion still fails but the agent is reset either way. --- src/functionsConfig.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/functionsConfig.spec.ts b/src/functionsConfig.spec.ts index 19393114fe2..ec16cc96917 100644 --- a/src/functionsConfig.spec.ts +++ b/src/functionsConfig.spec.ts @@ -58,7 +58,11 @@ describe("config.getFirebaseConfig", () => { }); afterEach(() => { - expect(nock.isDone()).to.be.true; + // Read isDone before cleaning, but clean before asserting, so a test that + // fails before its interceptor is used doesn't leak it into the next test. + const isDone = nock.isDone(); + nock.cleanAll(); + expect(isDone).to.equal(true, "all nock stubs should have been called"); }); it("should return the admin SDK config on success", async () => {