From f8dfd9de33b4ebb8b7f510ec191545ed6d65a087 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Wed, 3 Jun 2026 13:12:12 -0400 Subject: [PATCH 1/2] Improve error parsing for billing errors --- src/apiv2.spec.ts | 14 ++++++++++++++ src/apiv2.ts | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/apiv2.spec.ts b/src/apiv2.spec.ts index 96652879b6f..1de401aa59d 100644 --- a/src/apiv2.spec.ts +++ b/src/apiv2.spec.ts @@ -162,6 +162,20 @@ describe("apiv2", () => { expect(nock.isDone()).to.be.true; }); + it("should handle non-JSON error responses gracefully even when responseType is json", async () => { + const xml = + "BillingDisabled"; + nock("https://example.com").get("/path/to/foo").reply(403, xml); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = c.request({ + method: "GET", + path: "/path/to/foo", + }); + await expect(r).to.eventually.be.rejectedWith(FirebaseError, /BillingDisabled/); + expect(nock.isDone()).to.be.true; + }); + it("should error with a FirebaseError if an error happens", async () => { nock("https://example.com").get("/path/to/foo").replyWithError("boom"); diff --git a/src/apiv2.ts b/src/apiv2.ts index c9b716f9609..e012bf216cf 100644 --- a/src/apiv2.ts +++ b/src/apiv2.ts @@ -477,7 +477,11 @@ export class Client { } catch (err: unknown) { // JSON-parse errors are useless. Log the response for better debugging. this.logResponse(res, text, options); - throw new FirebaseError(`Unable to parse JSON: ${err}`); + if (res.status >= 400) { + body = text as unknown as ResT; + } else { + throw new FirebaseError(`Unable to parse JSON: ${err}`); + } } } } else if (options.responseType === "xml") { From d3bcc5633f4c17e8b4ff6c1e54c9be6e99903b5d Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Tue, 16 Jun 2026 20:12:28 -0400 Subject: [PATCH 2/2] clean up error handling for billing specifically --- src/apphosting/secrets/index.spec.ts | 18 ++++++++++++++++++ src/apphosting/secrets/index.ts | 3 ++- src/error.ts | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/apphosting/secrets/index.spec.ts b/src/apphosting/secrets/index.spec.ts index 5dc86eb8253..e76c8f36597 100644 --- a/src/apphosting/secrets/index.spec.ts +++ b/src/apphosting/secrets/index.spec.ts @@ -189,6 +189,24 @@ describe("secrets", () => { undefined, ); }); + + it("appends original message for 403 errors", async () => { + gcsm.getSecret.withArgs("project", "secret").rejects(new Error("Forbidden")); + + await expect(secrets.upsertSecret("project", "secret")).to.be.rejectedWith("Unexpected error loading secret: Forbidden"); + }); + + it("appends original message for 400 errors", async () => { + gcsm.getSecret.withArgs("project", "secret").rejects(new Error("Bad Request")); + + await expect(secrets.upsertSecret("project", "secret")).to.be.rejectedWith("Unexpected error loading secret: Bad Request"); + }); + + it("appends original message for other errors", async () => { + gcsm.getSecret.withArgs("project", "secret").rejects(new Error("Internal Error")); + + await expect(secrets.upsertSecret("project", "secret")).to.be.rejectedWith("Unexpected error loading secret: Internal Error"); + }); }); describe("toMulti", () => { diff --git a/src/apphosting/secrets/index.ts b/src/apphosting/secrets/index.ts index c85f0645abc..531b5b09f19 100644 --- a/src/apphosting/secrets/index.ts +++ b/src/apphosting/secrets/index.ts @@ -198,7 +198,8 @@ export async function upsertSecret( existing = await gcsm.getSecret(project, secret); } catch (err: unknown) { if (getErrStatus(err) !== 404) { - throw new FirebaseError("Unexpected error loading secret", { original: getError(err) }); + const original = getError(err); + throw new FirebaseError(`Unexpected error loading secret: ${original.message}`, { original }); } await gcsm.createSecret(project, secret, gcsm.labels("apphosting"), location); return true; diff --git a/src/error.ts b/src/error.ts index e85ddeaef71..30db9bb8b83 100644 --- a/src/error.ts +++ b/src/error.ts @@ -117,7 +117,8 @@ export function isBillingError(e: { return !!e.context?.body?.error?.details?.find((d) => { return ( d.violations?.find((v) => v.type === "serviceusage/billing-enabled") || - d.reason === "UREQ_PROJECT_BILLING_NOT_FOUND" + d.reason === "UREQ_PROJECT_BILLING_NOT_FOUND" || + d.reason === "BILLING_DISABLED" ); }); }