Skip to content

Commit

Permalink
fix: decode Http error using proto3-JSON-serializer (#1273)
Browse files Browse the repository at this point in the history
  • Loading branch information
summer-ji-eng authored May 25, 2022
1 parent 337eaae commit 17a790f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 27 deletions.
5 changes: 2 additions & 3 deletions src/fallbackServiceStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function generateServiceStub(
const cancelController = hasAbortController()
? new AbortController()
: new NodeAbortController();
const cancelSignal = cancelController.signal;
const cancelSignal = cancelController.signal as AbortSignal;
let cancelRequested = false;

const fetchParameters = requestEncoder(
Expand All @@ -109,7 +109,7 @@ export function generateServiceStub(
authClient
.getRequestHeaders()
.then(authHeader => {
const fetchRequest = {
const fetchRequest: RequestInit = {
headers: {
...authHeader,
...headers,
Expand All @@ -128,7 +128,6 @@ export function generateServiceStub(
) {
delete fetchRequest['body'];
}

return fetch(url, fetchRequest);
})
.then((response: Response | NodeFetchResponse) => {
Expand Down
82 changes: 64 additions & 18 deletions src/googleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import {Status, rpcCodeFromHttpStatusCode} from './status';
import * as protobuf from 'protobufjs';
import {Metadata} from './grpc';
import * as serializer from 'proto3-json-serializer';
import {defaultToObjectOptions} from './fallback';
import {JSONValue} from 'proto3-json-serializer';

export class GoogleError extends Error {
code?: Status;
Expand Down Expand Up @@ -59,33 +62,44 @@ 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 {
const decoder = new GoogleErrorDecoder();
const proto3Error = decoder.decodeHTTPError(json['error']);
const error = Object.assign(
new GoogleError(json['error']['message']),
json.error
proto3Error
);
// Map Http Status Code to gRPC Status Code
if (json['error']['code']) {
error.code = rpcCodeFromHttpStatusCode(json['error']['code']);
} else {
// If error code is absent, proto3 message default value is 0. We should
// keep error code as undefined.
delete error.code;
}

// Keep consistency with gRPC statusDetails fields. gRPC details has been occupied before.
// Rename "detials" to "statusDetails".
error.statusDetails = json['error']['details'];
delete error.details;
// Promote the ErrorInfo fields as error's top-level.
const errorInfo = !json['error']['details']
? undefined
: json['error']['details'].find(
(item: {[x: string]: string}) =>
item['@type'] === 'type.googleapis.com/google.rpc.ErrorInfo'
);
if (errorInfo) {
error.reason = errorInfo.reason;
error.domain = errorInfo.domain;
// error.metadata has been occupied for gRPC metadata, so we use
// errorInfoMetadat to represent ErrorInfo' metadata field. Keep
// consistency with gRPC ErrorInfo metadata field name.
error.errorInfoMetadata = errorInfo.metadata;
if (error.details) {
try {
const statusDetailsObj: GRPCStatusDetailsObject =
decoder.decodeHttpStatusDetails(error.details);
if (
statusDetailsObj &&
statusDetailsObj.details &&
statusDetailsObj.details.length > 0
) {
error.statusDetails = statusDetailsObj.details;
}
if (statusDetailsObj && statusDetailsObj.errorInfo) {
error.reason = statusDetailsObj.errorInfo.reason;
error.domain = statusDetailsObj.errorInfo.domain;
// error.metadata has been occupied for gRPC metadata, so we use
// errorInfoMetadata to represent ErrorInfo' metadata field. Keep
// consistency with gRPC ErrorInfo metadata field name.
error.errorInfoMetadata = statusDetailsObj.errorInfo.metadata;
}
} catch (decodeErr) {
// ignoring the error
}
}
return error;
}
Expand Down Expand Up @@ -224,4 +238,36 @@ export class GoogleErrorDecoder {
};
return result;
}

// Decodes http error which is an instance of google.rpc.Status.
decodeHTTPError(json: JSONValue) {
const errorMessage = serializer.fromProto3JSON(this.statusType, json);
if (!errorMessage) {
throw new Error(
`Received error message ${json}, but failed to serialize as proto3 message`
);
}
return this.statusType.toObject(errorMessage, defaultToObjectOptions);
}

// Decodes http error details which is an instance of Array<google.protobuf.Any>.
decodeHttpStatusDetails(
rawDetails: Array<ProtobufAny>
): GRPCStatusDetailsObject {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const details: protobuf.Message<{}>[] = [];
let errorInfo;
for (const detail of rawDetails) {
try {
const decodedDetail = this.decodeProtobufAny(detail);
details.push(decodedDetail);
if (detail.type_url === 'type.googleapis.com/google.rpc.ErrorInfo') {
errorInfo = decodedDetail as unknown as ErrorInfo;
}
} catch (err) {
// cannot decode detail, likely because of the unknown type - just skip it
}
}
return {details, errorInfo};
}
}
7 changes: 3 additions & 4 deletions src/streamArrayParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class StreamArrayParser extends Transform {
private _isSkipped: boolean;
private _level: number;
rpc: protobuf.Method;
cancelController: AbortController;
cancelController: AbortController | NodeAbortController;
cancelSignal: AbortSignal;
cancelRequested: boolean;
/**
Expand Down Expand Up @@ -60,10 +60,9 @@ export class StreamArrayParser extends Transform {
this._level = 0;
this.rpc = rpc;
this.cancelController = hasAbortController()
? // eslint-disable-next-line no-undef
new AbortController()
? new AbortController()
: new NodeAbortController();
this.cancelSignal = this.cancelController.signal;
this.cancelSignal = this.cancelController.signal as AbortSignal;
this.cancelRequested = false;
}

Expand Down
45 changes: 45 additions & 0 deletions test/unit/googleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,48 @@ describe('map http status code to gRPC status code', () => {
assert.deepStrictEqual(error.code, undefined);
});
});

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 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)
);
});
});
4 changes: 2 additions & 2 deletions test/unit/grpc-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ describe('grpc-fallback', () => {
echoStub.echo(requestObject, {}, {}, (err?: Error) => {
assert(err instanceof GoogleError);
assert.strictEqual(
JSON.stringify(err.statusDetails),
JSON.stringify(serverError['error']['details'])
JSON.stringify(err.statusDetails?.length),
JSON.stringify(serverError['error']['details'].length)
);
assert.strictEqual(err.code, 7);
assert.strictEqual(err.message, serverError['error']['message']);
Expand Down

0 comments on commit 17a790f

Please sign in to comment.