Skip to content

Commit 51ab466

Browse files
committed
fix(data-connect): include debugDetails in GraphQL error messages
1 parent 7cf116b commit 51ab466

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

‎src/data-connect/data-connect-api-client-internal.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ export class DataConnectApiClient {
407407
};
408408
const resp = await this.httpClient.send(request);
409409
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) {
410-
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' ');
410+
const allMessages = formatGraphqlErrors(resp.data.errors);
411411
throw new FirebaseDataConnectError({
412412
code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR,
413413
message: allMessages,
@@ -435,6 +435,15 @@ export class DataConnectApiClient {
435435
}
436436

437437
const data = response.data as any;
438+
if (validator.isNonNullObject(data) && validator.isNonEmptyArray(data.errors)) {
439+
return new FirebaseDataConnectError({
440+
code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR,
441+
message: formatGraphqlErrors(data.errors),
442+
httpResponse: toHttpResponse(response),
443+
cause: err,
444+
});
445+
}
446+
438447
const error: ServerError = (validator.isNonNullObject(data) && validator.isNonNullObject(data.error))
439448
? data.error
440449
: (validator.isNonNullObject(data) ? data : {});
@@ -646,6 +655,30 @@ interface ServerError {
646655
status?: string;
647656
}
648657

658+
interface GraphqlErrorResponse {
659+
message?: string;
660+
extensions?: {
661+
debugDetails?: string;
662+
[key: string]: any;
663+
};
664+
[key: string]: any;
665+
}
666+
667+
/**
668+
* Formats GraphQL errors into a human-readable string, including debugDetails if present.
669+
*
670+
* @internal
671+
*/
672+
export function formatGraphqlErrors(errors: GraphqlErrorResponse[]): string {
673+
return errors
674+
.map((error) => {
675+
const message = error.message || 'Unknown error';
676+
const details = error.extensions?.debugDetails;
677+
return details ? `${message}: ${details}` : message;
678+
})
679+
.join('; ');
680+
}
681+
649682
/**
650683
* Extracts property keys from an object or array of objects as a space-separated string,
651684
* including recursively nested object/array fields for the `@allow(fields: ...)` directive.

‎test/unit/data-connect/data-connect-api-client-internal.spec.ts‎

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@
1616
*/
1717

1818
import * as _ from 'lodash';
19-
import { expect } from 'chai';
19+
import * as chai from 'chai';
2020
import * as sinon from 'sinon';
21+
import * as sinonChai from 'sinon-chai';
22+
import * as chaiAsPromised from 'chai-as-promised';
23+
24+
chai.should();
25+
chai.use(sinonChai);
26+
chai.use(chaiAsPromised);
27+
28+
const expect = chai.expect;
2129
import {
2230
AuthorizedHttpClient,
2331
HttpClient,
@@ -252,6 +260,72 @@ describe('DataConnectApiClient', () => {
252260
return apiClient.executeGraphql('query', {})
253261
.should.eventually.be.rejected.and.deep.include(expected);
254262
});
263+
264+
it('should reject when GraphQL errors with debugDetails are returned', async () => {
265+
const gqlErrorResponse = {
266+
errors: [
267+
{
268+
message: 'SQL execution failed',
269+
path: ['content_update'],
270+
extensions: {
271+
code: 'INTERNAL',
272+
debugDetails: 'Quota exceeded for quota metric Connect Queries'
273+
}
274+
}
275+
]
276+
};
277+
sandbox
278+
.stub(HttpClient.prototype, 'send')
279+
.resolves(utils.responseFrom(gqlErrorResponse, 200));
280+
281+
await expect(apiClient.executeGraphql('query', {}))
282+
.to.be.rejectedWith(
283+
FirebaseDataConnectError,
284+
'SQL execution failed: Quota exceeded for quota metric Connect Queries'
285+
);
286+
});
287+
288+
it('should reject and format multiple GraphQL errors', async () => {
289+
const gqlErrorResponse = {
290+
errors: [
291+
{ message: 'First error' },
292+
{
293+
message: 'Second error',
294+
extensions: { debugDetails: 'detailed failure' }
295+
}
296+
]
297+
};
298+
sandbox
299+
.stub(HttpClient.prototype, 'send')
300+
.resolves(utils.responseFrom(gqlErrorResponse, 200));
301+
302+
await expect(apiClient.executeGraphql('query', {}))
303+
.to.be.rejectedWith(
304+
FirebaseDataConnectError,
305+
'First error; Second error: detailed failure'
306+
);
307+
});
308+
309+
it('should reject with formatted GraphQL errors on non-200 HTTP response', async () => {
310+
const gqlErrorResponse = {
311+
errors: [
312+
{
313+
message: 'Bad request',
314+
extensions: { debugDetails: 'Field not found' }
315+
}
316+
]
317+
};
318+
const mockErr = utils.errorFrom(gqlErrorResponse, 400);
319+
sandbox
320+
.stub(HttpClient.prototype, 'send')
321+
.rejects(mockErr);
322+
323+
await expect(apiClient.executeGraphql('query', {}))
324+
.to.be.rejectedWith(
325+
FirebaseDataConnectError,
326+
'Bad request: Field not found'
327+
);
328+
});
255329
});
256330

257331
it('should resolve with the GraphQL response on success', async () => {

0 commit comments

Comments
 (0)