Skip to content

Commit

Permalink
fix: parse error from stream call (#1275)
Browse files Browse the repository at this point in the history
  • Loading branch information
summer-ji-eng authored May 26, 2022
1 parent 17a790f commit 141a587
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 27 deletions.
6 changes: 6 additions & 0 deletions src/googleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export class GoogleError extends Error {
// Parse http JSON error and promote google.rpc.ErrorInfo if exist.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static parseHttpError(json: any): GoogleError {
if (Array.isArray(json)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
json = json.find((obj: any) => {
return 'error' in obj;
});
}
const decoder = new GoogleErrorDecoder();
const proto3Error = decoder.decodeHTTPError(json['error']);
const error = Object.assign(
Expand Down
4 changes: 4 additions & 0 deletions src/streamingCalls/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
SimpleCallbackFunction,
} from '../apitypes';
import {RetryRequestOptions} from '../gax';
import {GoogleError} from '../googleError';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const duplexify: DuplexifyConstructor = require('duplexify');
Expand Down Expand Up @@ -152,6 +153,9 @@ export class StreamProxy extends duplexify implements GRPCCallResult {
});
this._responseHasSent = true;
});
stream.on('error', error => {
GoogleError.parseGRPCStatusDetails(error);
});
}

/**
Expand Down
70 changes: 43 additions & 27 deletions test/unit/googleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,34 +305,34 @@ describe('map http status code to gRPC status code', () => {
});

describe('http error decoding', () => {
it('should promote ErrorInfo if exist in http error', () => {
const errorInfo = {
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
reason: 'SERVICE_DISABLED',
domain: 'googleapis.com',
metadata: {
service: 'translate.googleapis.com',
consumer: 'projects/123',
},
};
const help = {
'@type': 'type.googleapis.com/google.rpc.Help',
links: [
{
description: 'Google developers console API activation',
url: 'https://console.developers.google.com/apis/api/translate.googleapis.com/overview?project=455411330361',
},
],
};
const json = {
error: {
code: 403,
message:
'Cloud Translation API has not been used in project 123 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/translate.googleapis.com/overview?project=455411330361 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.',
status: 'PERMISSION_DENIED',
details: [help, errorInfo],
const errorInfo = {
'@type': 'type.googleapis.com/google.rpc.ErrorInfo',
reason: 'SERVICE_DISABLED',
domain: 'googleapis.com',
metadata: {
service: 'translate.googleapis.com',
consumer: 'projects/123',
},
};
const help = {
'@type': 'type.googleapis.com/google.rpc.Help',
links: [
{
description: 'Google developers console API activation',
url: 'https://console.developers.google.com/apis/api/translate.googleapis.com/overview?project=455411330361',
},
};
],
};
const json = {
error: {
code: 403,
message:
'Cloud Translation API has not been used in project 123 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/translate.googleapis.com/overview?project=455411330361 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.',
status: 'PERMISSION_DENIED',
details: [help, errorInfo],
},
};
it('should promote ErrorInfo if exist in http error', () => {
const error = GoogleError.parseHttpError(json);
assert.deepStrictEqual(error.code, rpcCodeFromHttpStatusCode(403));
assert.deepStrictEqual(
Expand All @@ -347,4 +347,20 @@ describe('http error decoding', () => {
JSON.stringify(errorInfo.metadata)
);
});

it('should support http error in array', () => {
const error = GoogleError.parseHttpError([json]);
assert.deepStrictEqual(error.code, rpcCodeFromHttpStatusCode(403));
assert.deepStrictEqual(
error.statusDetails?.length,
json['error']['details'].length
);
assert.deepStrictEqual(error.message, json['error']['message']);
assert.deepStrictEqual(error.reason, errorInfo.reason);
assert.deepStrictEqual(error.domain, errorInfo.domain);
assert.deepStrictEqual(
JSON.stringify(error.errorInfoMetadata),
JSON.stringify(errorInfo.metadata)
);
});
});
59 changes: 59 additions & 0 deletions test/unit/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import internal = require('stream');
import {StreamArrayParser} from '../../src/streamArrayParser';
import path = require('path');
import protobuf = require('protobufjs');
import {GoogleError} from '../../src';
import {CodeChallengeMethod} from 'google-auth-library';
import {Metadata} from '@grpc/grpc-js';

function createApiCallStreaming(
func:
Expand Down Expand Up @@ -458,6 +461,62 @@ describe('streaming', () => {
assert.strictEqual(responseCallback.callCount, 1);
});
});

it('emit parsed GoogleError', done => {
const errorInfoObj = {
reason: 'SERVICE_DISABLED',
domain: 'googleapis.com',
metadata: {
consumer: 'projects/455411330361',
service: 'translate.googleapis.com',
},
};
const errorProtoJson = require('../../protos/status.json');
const root = protobuf.Root.fromJSON(errorProtoJson);
const errorInfoType = root.lookupType('ErrorInfo');
const buffer = errorInfoType.encode(errorInfoObj).finish() as Buffer;
const any = {
type_url: 'type.googleapis.com/google.rpc.ErrorInfo',
value: buffer,
};
const status = {code: 3, message: 'test', details: [any]};
const Status = root.lookupType('google.rpc.Status');
const status_buffer = Status.encode(status).finish() as Buffer;
const metadata = new Metadata();
metadata.set('grpc-status-details-bin', status_buffer);
const error = Object.assign(new GoogleError('test error'), {
code: 5,
details: 'Failed to read',
metadata: metadata,
});
const spy = sinon.spy((...args: Array<{}>) => {
assert.strictEqual(args.length, 3);
const s = new PassThrough({
objectMode: true,
});
s.push(null);
setImmediate(() => {
s.emit('error', error);
});
return s;
});
const apiCall = createApiCallStreaming(
spy,
streaming.StreamType.SERVER_STREAMING
);
const s = apiCall({}, undefined);
s.on('error', err => {
assert(err instanceof GoogleError);
assert.deepStrictEqual(err.message, 'test error');
assert.strictEqual(err.domain, errorInfoObj.domain);
assert.strictEqual(err.reason, errorInfoObj.reason);
assert.strictEqual(
JSON.stringify(err.errorInfoMetadata),
JSON.stringify(errorInfoObj.metadata)
);
done();
});
});
});

describe('REST streaming apiCall return StreamArrayParser', () => {
Expand Down

0 comments on commit 141a587

Please sign in to comment.