Skip to content

Commit

Permalink
feat: properly decode error in fallback scenario, export FallbackServ…
Browse files Browse the repository at this point in the history
…iceError type (#866)

* fix: properly decode error in fallback scenario

* feat: pr feedback, export FallbackServiceError type
  • Loading branch information
alexander-fenster authored Jul 10, 2020
1 parent 071f4e9 commit af15e53
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 18 deletions.
8 changes: 4 additions & 4 deletions src/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));
})
Expand Down
30 changes: 23 additions & 7 deletions src/fallbackError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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));
}
}
9 changes: 5 additions & 4 deletions test/browser-test/test.grpc-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
});
});
Expand Down
32 changes: 32 additions & 0 deletions test/unit/fallbackError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
});
});
9 changes: 6 additions & 3 deletions test/unit/grpc-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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();
});
});
Expand Down

0 comments on commit af15e53

Please sign in to comment.