Skip to content

Commit 22255fb

Browse files
fix(dataconnect): fix deserialization of emulator gRPC error codes into detailed errors (#3180)
* initial fix * address reviewer comments * update gitignore * improve description of emulator grpc error code mapping const
1 parent 9c792b3 commit 22255fb

4 files changed

Lines changed: 66 additions & 9 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ firebase-admin-*.tgz
2222
docgen/markdown/
2323

2424
# Dataconnect integration test artifacts should not be checked in
25-
test/integration/dataconnect/dataconnect/.dataconnect
25+
test/integration/dataconnect/dataconnect/.dataconnect
26+
test/integration/dataconnect/*.log

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import {
2121
HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient
2222
} from '../utils/api-request';
2323
import { FirebaseError, toHttpResponse } from '../utils/error';
24-
import { FirebaseDataConnectError, DataConnectErrorCode, DATA_CONNECT_ERROR_CODE_MAPPING } from './error';
24+
import {
25+
FirebaseDataConnectError,
26+
DataConnectErrorCode,
27+
DATA_CONNECT_ERROR_CODE_MAPPING,
28+
EMULATOR_GRPC_STATUS_CODE_TO_STRING
29+
} from './error';
2530
import * as utils from '../utils/index';
2631
import * as validator from '../utils/validator';
2732
import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions, OperationOptions } from './data-connect-api';
@@ -409,10 +414,19 @@ export class DataConnectApiClient {
409414
});
410415
}
411416

412-
const error: ServerError = (response.data as ErrorResponse).error || {};
417+
const data = response.data as any;
418+
const error: ServerError = (validator.isNonNullObject(data) && validator.isNonNullObject(data.error))
419+
? data.error
420+
: (validator.isNonNullObject(data) ? data : {});
421+
422+
let status = error.status;
423+
if (!status && validator.isNumber(error.code)) {
424+
status = EMULATOR_GRPC_STATUS_CODE_TO_STRING[error.code as number];
425+
}
426+
413427
let code: DataConnectErrorCode = DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN;
414-
if (error.status && error.status in DATA_CONNECT_ERROR_CODE_MAPPING) {
415-
code = DATA_CONNECT_ERROR_CODE_MAPPING[error.status];
428+
if (status && status in DATA_CONNECT_ERROR_CODE_MAPPING) {
429+
code = DATA_CONNECT_ERROR_CODE_MAPPING[status];
416430
}
417431
const message = error.message || 'Unknown server error';
418432
return new FirebaseDataConnectError({
@@ -670,10 +684,6 @@ export function useEmulator(): boolean {
670684
return !!emulatorHost();
671685
}
672686

673-
interface ErrorResponse {
674-
error?: ServerError;
675-
}
676-
677687
interface ServerError {
678688
code?: number;
679689
message?: string;

src/data-connect/error.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ export class FirebaseDataConnectError extends FirebaseError {
6969
});
7070
}
7171
}
72+
73+
/**
74+
* Mappings from gRPC status codes to their string equivalents. Used to convert
75+
* error codes from the emulator into the codes and statuses matching errors
76+
* from production.
77+
* @internal
78+
*/
79+
export const EMULATOR_GRPC_STATUS_CODE_TO_STRING: Record<number, string> = {
80+
1: 'CANCELLED',
81+
2: 'UNKNOWN',
82+
3: 'INVALID_ARGUMENT',
83+
4: 'DEADLINE_EXCEEDED',
84+
5: 'NOT_FOUND',
85+
6: 'ALREADY_EXISTS',
86+
7: 'PERMISSION_DENIED',
87+
8: 'RESOURCE_EXHAUSTED',
88+
9: 'FAILED_PRECONDITION',
89+
10: 'ABORTED',
90+
11: 'OUT_OF_RANGE',
91+
12: 'UNIMPLEMENTED',
92+
13: 'INTERNAL',
93+
14: 'UNAVAILABLE',
94+
15: 'DATA_LOSS',
95+
16: 'UNAUTHENTICATED',
96+
};

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,27 @@ describe('DataConnectApiClient', () => {
179179
.and.have.property('cause', expected.cause);
180180
});
181181

182+
it('should reject when a gRPC-to-HTTP transcoded error response is received', () => {
183+
const grpcError = {
184+
code: 7,
185+
message: 'Permission denied',
186+
};
187+
const mockErr = utils.errorFrom(grpcError, 403);
188+
sandbox
189+
.stub(HttpClient.prototype, 'send')
190+
.rejects(mockErr);
191+
const expected = new FirebaseDataConnectError({
192+
code: 'permission-denied',
193+
message: 'Permission denied',
194+
httpResponse: toHttpResponse(mockErr.response),
195+
cause: mockErr
196+
});
197+
return apiClient.executeGraphql('query', {})
198+
.should.eventually.be.rejected
199+
.and.deep.include(expected)
200+
.and.have.property('cause', expected.cause);
201+
});
202+
182203
it('should reject with unknown-error when error code is not present', () => {
183204
const mockErr = utils.errorFrom({}, 404);
184205
sandbox

0 commit comments

Comments
 (0)