diff --git a/src/fallback.ts b/src/fallback.ts index 7e3eccb51..05e402d11 100644 --- a/src/fallback.ts +++ b/src/fallback.ts @@ -35,8 +35,8 @@ import {GaxCall, GRPCCall} from './apitypes'; import {Descriptor} from './descriptor'; import {createApiCall as _createApiCall} from './createApiCall'; import {isBrowser} from './isbrowser'; -import {FallbackErrorDecoder} from './fallbackError'; - +import {FallbackErrorDecoder, FallbackServiceError} from './fallbackError'; +export {FallbackServiceError}; export {PathTemplate} from './pathTemplate'; export {routingHeader}; export {CallSettings, constructSettings, RetryOptions} from './gax'; @@ -333,8 +333,8 @@ export class GrpcClient { }) .then(([ok, buffer]: [boolean, Buffer | ArrayBuffer]) => { if (!ok) { - const status = statusDecoder.decodeRpcStatus(buffer); - throw new Error(JSON.stringify(status)); + const error = statusDecoder.decodeErrorFromBuffer(buffer); + throw error; } serviceCallback(null, new Uint8Array(buffer)); }) diff --git a/src/fallbackError.ts b/src/fallbackError.ts index 346e07136..bca927628 100644 --- a/src/fallbackError.ts +++ b/src/fallbackError.ts @@ -15,6 +15,15 @@ */ import * as protobuf from 'protobufjs'; +import {Status} from './status'; + +// A copy of gRPC ServiceError but with no metadata +export type FallbackServiceError = FallbackStatusObject & Error; +interface FallbackStatusObject { + code: Status; + message: string; + details: Array<{}>; +} interface ProtobufAny { type_url: string; @@ -27,12 +36,6 @@ interface RpcStatus { details: ProtobufAny[]; } -interface DecodedRpcStatus { - code: number; - message: string; - details: Array<{}>; -} - export class FallbackErrorDecoder { root: protobuf.Root; anyType: protobuf.Type; @@ -62,7 +65,7 @@ export class FallbackErrorDecoder { } // Decodes gRPC-fallback error which is an instance of google.rpc.Status. - decodeRpcStatus(buffer: Buffer | ArrayBuffer): DecodedRpcStatus { + decodeRpcStatus(buffer: Buffer | ArrayBuffer): FallbackStatusObject { const uint8array = new Uint8Array(buffer); const status = (this.statusType.decode(uint8array) as unknown) as RpcStatus; @@ -75,4 +78,17 @@ export class FallbackErrorDecoder { }; return result; } + + // Construct an Error from a StatusObject. + // Adapted from https://github.com/grpc/grpc-node/blob/master/packages/grpc-js/src/call.ts#L79 + callErrorFromStatus(status: FallbackStatusObject): FallbackServiceError { + status.message = `${status.code} ${Status[status.code]}: ${status.message}`; + return Object.assign(new Error(status.message), status); + } + + // Decodes gRPC-fallback error which is an instance of google.rpc.Status, + // and puts it into the object similar to gRPC ServiceError object. + decodeErrorFromBuffer(buffer: Buffer | ArrayBuffer): Error { + return this.callErrorFromStatus(this.decodeRpcStatus(buffer)); + } } diff --git a/test/browser-test/test.grpc-fallback.ts b/test/browser-test/test.grpc-fallback.ts index 8159dddff..1d0ca10a8 100644 --- a/test/browser-test/test.grpc-fallback.ts +++ b/test/browser-test/test.grpc-fallback.ts @@ -252,11 +252,10 @@ describe('grpc-fallback', () => { it('should handle an error', done => { const requestObject = {content: 'test-content'}; // example of an actual google.rpc.Status error message returned by Language API - const expectedError = { + const expectedError = Object.assign(new Error('Error message'), { code: 3, - message: 'Error message', details: [], - }; + }); const fakeFetch = sinon.fake.resolves({ ok: false, @@ -272,7 +271,9 @@ describe('grpc-fallback', () => { gaxGrpc.createStub(echoService, stubOptions).then(echoStub => { echoStub.echo(requestObject, {}, {}, (err: Error) => { - assert.strictEqual(err.message, JSON.stringify(expectedError)); + assert(err instanceof Error); + assert.strictEqual(err.message, '3 INVALID_ARGUMENT: Error message'); + assert.strictEqual(JSON.stringify(err), JSON.stringify(expectedError)); done(); }); }); diff --git a/test/unit/fallbackError.ts b/test/unit/fallbackError.ts index 31c1f7e35..f341de061 100644 --- a/test/unit/fallbackError.ts +++ b/test/unit/fallbackError.ts @@ -48,4 +48,36 @@ describe('gRPC-fallback error decoding', () => { JSON.stringify(expectedError) ); }); + + it('decodes error and status code', () => { + // example of an actual google.rpc.Status error message returned by Language API + const fixtureName = path.resolve(__dirname, '..', 'fixtures', 'error.bin'); + const errorBin = fs.readFileSync(fixtureName); + const expectedError = Object.assign( + new Error( + '3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set.' + ), + { + code: 3, + details: [ + { + fieldViolations: [ + { + field: 'document.content', + description: 'Must have some text content to annotate.', + }, + ], + }, + ], + } + ); + const decoder = new FallbackErrorDecoder(); + const decodedError = decoder.decodeErrorFromBuffer(errorBin); + assert(decodedError instanceof Error); + // nested error messages have different types so we can't use deepStrictEqual here + assert.strictEqual( + JSON.stringify(decodedError), + JSON.stringify(expectedError) + ); + }); }); diff --git a/test/unit/grpc-fallback.ts b/test/unit/grpc-fallback.ts index b96a53407..6f447012a 100644 --- a/test/unit/grpc-fallback.ts +++ b/test/unit/grpc-fallback.ts @@ -242,9 +242,10 @@ describe('grpc-fallback', () => { // example of an actual google.rpc.Status error message returned by Language API const fixtureName = path.resolve(__dirname, '..', 'fixtures', 'error.bin'); const errorBin = fs.readFileSync(fixtureName); + const expectedMessage = + '3 INVALID_ARGUMENT: One of content, or gcs_content_uri must be set.'; const expectedError = { code: 3, - message: 'One of content, or gcs_content_uri must be set.', details: [ { fieldViolations: [ @@ -267,8 +268,10 @@ describe('grpc-fallback', () => { ); gaxGrpc.createStub(echoService, stubOptions).then(echoStub => { - echoStub.echo(requestObject, {}, {}, (err: {message: string}) => { - assert.strictEqual(err.message, JSON.stringify(expectedError)); + echoStub.echo(requestObject, {}, {}, (err: Error) => { + assert(err instanceof Error); + assert.strictEqual(err.message, expectedMessage); + assert.strictEqual(JSON.stringify(err), JSON.stringify(expectedError)); done(); }); });