Skip to content

Commit dd7c567

Browse files
authored
fix: Include detailed error information from orchestration API responses (#9377)
1 parent 446c58c commit dd7c567

File tree

4 files changed

+366
-5
lines changed

4 files changed

+366
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
- Added `functions.list_functions` as a MCP tool (#9369)
22
- Added AI Logic to `firebase init` CLI command and `firebase_init` MCP tool. (#9185)
3+
- Improved error messages for Firebase AI Logic provisioning during 'firebase init' (#9377)
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { expect } from "chai";
2+
import { FirebaseError } from "../../error";
3+
import { enhanceProvisioningError } from "./errorHandler";
4+
5+
describe("errorHandler", () => {
6+
describe("enhanceProvisioningError", () => {
7+
it("should include ErrorInfo details in error message", () => {
8+
const originalError = new FirebaseError("Permission denied", {
9+
context: {
10+
body: {
11+
error: {
12+
code: 403,
13+
message: "The user has not accepted the terms of service.",
14+
status: "PERMISSION_DENIED",
15+
details: [
16+
{
17+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
18+
reason:
19+
"TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].",
20+
domain: "firebase.googleapis.com",
21+
},
22+
],
23+
},
24+
},
25+
},
26+
});
27+
28+
const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app");
29+
30+
expect(result).to.be.instanceOf(FirebaseError);
31+
expect(result.message).to.include("Failed to provision Firebase app: Permission denied");
32+
expect(result.message).to.include("Error details:");
33+
expect(result.message).to.include(
34+
"Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].",
35+
);
36+
expect(result.message).to.include("Domain: firebase.googleapis.com");
37+
expect(result.exit).to.equal(2);
38+
expect(result.original).to.equal(originalError);
39+
});
40+
41+
it("should include HelpLinks in error message", () => {
42+
const originalError = new FirebaseError("Permission denied", {
43+
context: {
44+
body: {
45+
error: {
46+
code: 403,
47+
message: "The user has not accepted the terms of service.",
48+
status: "PERMISSION_DENIED",
49+
details: [
50+
{
51+
"@type": "type.googleapis.com/google.rpc.Help",
52+
links: [
53+
{
54+
description: "Link to accept Generative Language terms of service",
55+
url: "https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true",
56+
},
57+
],
58+
},
59+
],
60+
},
61+
},
62+
},
63+
});
64+
65+
const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app");
66+
67+
expect(result.message).to.include("For help resolving this issue:");
68+
expect(result.message).to.include("Link to accept Generative Language terms of service");
69+
expect(result.message).to.include(
70+
"https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true",
71+
);
72+
});
73+
74+
it("should include both ErrorInfo and HelpLinks in error message", () => {
75+
const originalError = new FirebaseError("Permission denied", {
76+
context: {
77+
body: {
78+
error: {
79+
code: 403,
80+
message: "The user has not accepted the terms of service.",
81+
status: "PERMISSION_DENIED",
82+
details: [
83+
{
84+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
85+
reason:
86+
"TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].",
87+
domain: "firebase.googleapis.com",
88+
},
89+
{
90+
"@type": "type.googleapis.com/google.rpc.Help",
91+
links: [
92+
{
93+
description: "Link to accept Generative Language terms of service",
94+
url: "https://console.cloud.google.com/apis/library/generativelanguage.googleapis.com?authuser=0&forceCheckTos=true",
95+
},
96+
],
97+
},
98+
],
99+
},
100+
},
101+
},
102+
});
103+
104+
const result = enhanceProvisioningError(originalError, "Failed to provision Firebase app");
105+
106+
// Verify ErrorInfo is included
107+
expect(result.message).to.include("Error details:");
108+
expect(result.message).to.include(
109+
"Reason: TOS_REQUIRED: The following ToS's must be accepted: [generative-language-api].",
110+
);
111+
expect(result.message).to.include("Domain: firebase.googleapis.com");
112+
113+
// Verify HelpLinks are included
114+
expect(result.message).to.include("For help resolving this issue:");
115+
expect(result.message).to.include("Link to accept Generative Language terms of service");
116+
});
117+
118+
it("should include ErrorInfo with metadata in error message", () => {
119+
const originalError = new FirebaseError("Invalid request", {
120+
context: {
121+
body: {
122+
error: {
123+
code: 400,
124+
message: "Invalid request",
125+
status: "INVALID_ARGUMENT",
126+
details: [
127+
{
128+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
129+
reason: "INVALID_FIELD",
130+
domain: "firebase.googleapis.com",
131+
metadata: {
132+
field: "displayName",
133+
constraint: "max_length",
134+
},
135+
},
136+
],
137+
},
138+
},
139+
},
140+
});
141+
142+
const result = enhanceProvisioningError(originalError, "Operation failed");
143+
144+
expect(result.message).to.include("Reason: INVALID_FIELD");
145+
expect(result.message).to.include("Domain: firebase.googleapis.com");
146+
expect(result.message).to.include("Additional Info:");
147+
expect(result.message).to.include("field");
148+
});
149+
150+
it("should include multiple help links in error message", () => {
151+
const originalError = new FirebaseError("Multiple help links", {
152+
context: {
153+
body: {
154+
error: {
155+
code: 403,
156+
message: "Permission denied",
157+
status: "PERMISSION_DENIED",
158+
details: [
159+
{
160+
"@type": "type.googleapis.com/google.rpc.Help",
161+
links: [
162+
{
163+
description: "First help link",
164+
url: "https://example.com/help1",
165+
},
166+
{
167+
description: "Second help link",
168+
url: "https://example.com/help2",
169+
},
170+
],
171+
},
172+
],
173+
},
174+
},
175+
},
176+
});
177+
178+
const result = enhanceProvisioningError(originalError, "Operation failed");
179+
180+
expect(result.message).to.include("First help link");
181+
expect(result.message).to.include("https://example.com/help1");
182+
expect(result.message).to.include("Second help link");
183+
expect(result.message).to.include("https://example.com/help2");
184+
});
185+
186+
it("should handle errors without details gracefully", () => {
187+
const originalError = new FirebaseError("Firebase error without details", {
188+
context: {
189+
body: {
190+
error: {
191+
code: 400,
192+
message: "Bad Request",
193+
status: "INVALID_ARGUMENT",
194+
},
195+
},
196+
},
197+
});
198+
199+
const result = enhanceProvisioningError(originalError, "Operation failed");
200+
201+
expect(result).to.be.instanceOf(FirebaseError);
202+
expect(result.message).to.equal("Operation failed: Firebase error without details");
203+
expect(result.exit).to.equal(2);
204+
});
205+
206+
it("should handle non-Error types gracefully", () => {
207+
const result = enhanceProvisioningError("String error", "Operation failed");
208+
209+
expect(result).to.be.instanceOf(FirebaseError);
210+
expect(result.message).to.equal("Operation failed: String error");
211+
expect(result.exit).to.equal(2);
212+
expect(result.original).to.be.instanceOf(Error);
213+
});
214+
215+
it("should handle regular Error without context", () => {
216+
const regularError = new Error("Regular error");
217+
218+
const result = enhanceProvisioningError(regularError, "Context message");
219+
220+
expect(result).to.be.instanceOf(FirebaseError);
221+
expect(result.message).to.equal("Context message: Regular error");
222+
expect(result.original).to.equal(regularError);
223+
});
224+
225+
it("should ignore unknown detail types", () => {
226+
const originalError = new FirebaseError("Unknown detail type", {
227+
context: {
228+
body: {
229+
error: {
230+
code: 500,
231+
message: "Internal error",
232+
status: "INTERNAL",
233+
details: [
234+
{
235+
"@type": "type.googleapis.com/google.rpc.UnknownType",
236+
someField: "someValue",
237+
},
238+
],
239+
},
240+
},
241+
},
242+
});
243+
244+
const result = enhanceProvisioningError(originalError, "Operation failed");
245+
246+
expect(result.message).to.equal("Operation failed: Unknown detail type");
247+
expect(result.message).to.not.include("Error details:");
248+
expect(result.message).to.not.include("For help");
249+
});
250+
});
251+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { FirebaseError, getError } from "../../error";
2+
3+
/**
4+
* Google RPC ErrorInfo structure
5+
*/
6+
interface ErrorInfo {
7+
"@type": "type.googleapis.com/google.rpc.ErrorInfo";
8+
reason: string;
9+
domain: string;
10+
metadata?: Record<string, string>;
11+
}
12+
13+
/**
14+
* Google RPC Help structure with links
15+
*/
16+
interface HelpLinks {
17+
"@type": "type.googleapis.com/google.rpc.Help";
18+
links: Array<{
19+
description: string;
20+
url: string;
21+
}>;
22+
}
23+
24+
/**
25+
* Error detail can be ErrorInfo, HelpLinks, or other types
26+
*/
27+
type ErrorDetail = ErrorInfo | HelpLinks | Record<string, unknown>;
28+
29+
/**
30+
* Provisioning API error structure
31+
*/
32+
interface ProvisioningError {
33+
error: {
34+
code: number;
35+
message: string;
36+
status: string;
37+
details?: ErrorDetail[];
38+
};
39+
}
40+
41+
/**
42+
* Type guard for ErrorInfo
43+
*/
44+
function isErrorInfo(detail: ErrorDetail): detail is ErrorInfo {
45+
return detail["@type"] === "type.googleapis.com/google.rpc.ErrorInfo";
46+
}
47+
48+
/**
49+
* Type guard for HelpLinks
50+
*/
51+
function isHelpLinks(detail: ErrorDetail): detail is HelpLinks {
52+
return detail["@type"] === "type.googleapis.com/google.rpc.Help";
53+
}
54+
55+
/**
56+
* Extracts detailed error information from a provisioning API error response.
57+
* Returns a formatted string with error details and help links.
58+
*/
59+
function extractErrorDetails(err: unknown): string {
60+
if (!(err instanceof Error)) {
61+
return "";
62+
}
63+
64+
// Check if this is a FirebaseError with context containing provisioning error
65+
if (err instanceof FirebaseError && err.context) {
66+
const context = err.context as { body?: ProvisioningError };
67+
const errorBody = context.body?.error;
68+
69+
if (errorBody?.details && Array.isArray(errorBody.details)) {
70+
const parts: string[] = [];
71+
72+
for (const detail of errorBody.details) {
73+
if (isErrorInfo(detail)) {
74+
parts.push(`Error details:`);
75+
parts.push(` Reason: ${detail.reason}`);
76+
parts.push(` Domain: ${detail.domain}`);
77+
if (detail.metadata) {
78+
parts.push(` Additional Info: ${JSON.stringify(detail.metadata)}`);
79+
}
80+
} else if (isHelpLinks(detail)) {
81+
parts.push(`\nFor help resolving this issue:`);
82+
for (const link of detail.links) {
83+
parts.push(` - ${link.description}`);
84+
parts.push(` ${link.url}`);
85+
}
86+
}
87+
}
88+
89+
return parts.length > 0 ? `\n\n${parts.join("\n")}` : "";
90+
}
91+
}
92+
93+
return "";
94+
}
95+
96+
/**
97+
* Enhances an error with detailed information from provisioning API responses.
98+
* This function extracts error details and includes them in the error message.
99+
*/
100+
export function enhanceProvisioningError(err: unknown, contextMessage: string): FirebaseError {
101+
const originalError = getError(err);
102+
const errorDetails = extractErrorDetails(err);
103+
104+
const fullMessage = errorDetails
105+
? `${contextMessage}: ${originalError.message}${errorDetails}`
106+
: `${contextMessage}: ${originalError.message}`;
107+
108+
return new FirebaseError(fullMessage, {
109+
exit: 2,
110+
original: originalError,
111+
});
112+
}

src/management/provisioning/provision.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger } from "../../logger";
55
import { pollOperation } from "../../operation-poller";
66
import { AppPlatform } from "../apps";
77
import * as types from "./types";
8+
import { enhanceProvisioningError } from "./errorHandler";
89

910
const apiClient = new Client({
1011
urlPrefix: firebaseApiOrigin(),
@@ -133,10 +134,6 @@ export async function provisionFirebaseApp(
133134
logger.debug("[provision] Firebase app provisioning completed successfully");
134135
return result;
135136
} catch (err: unknown) {
136-
const errorMessage = err instanceof Error ? err.message : String(err);
137-
throw new FirebaseError(`Failed to provision Firebase app: ${errorMessage}`, {
138-
exit: 2,
139-
original: err instanceof Error ? err : new Error(String(err)),
140-
});
137+
throw enhanceProvisioningError(err, "Failed to provision Firebase app");
141138
}
142139
}

0 commit comments

Comments
 (0)