From beabb800bd055649c2d18d62a69b6d135c9446c0 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Fri, 6 Sep 2024 01:49:53 +0200 Subject: [PATCH] feat: decode grpc status objects and map them to protocol types Status object thrown by grpc commands contains metadata that needs to be serialized in order to map it to custom errors generated through proto files https://github.com/grpc/grpc-web/issues/399 --- .../src/node/service-error.ts | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/src/node/service-error.ts b/arduino-ide-extension/src/node/service-error.ts index a56cf13ea..a42c05d8a 100644 --- a/arduino-ide-extension/src/node/service-error.ts +++ b/arduino-ide-extension/src/node/service-error.ts @@ -1,14 +1,54 @@ import { Metadata, StatusObject } from '@grpc/grpc-js'; +import { Status } from './cli-protocol/google/rpc/status_pb'; +import { stringToUint8Array } from '../common/utils'; +import { ProgrammerIsRequiredForUploadError } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; + +type ProtoError = typeof ProgrammerIsRequiredForUploadError; +const protoErrorsMap = new Map([ + [ + 'type.googleapis.com/cc.arduino.cli.commands.v1.ProgrammerIsRequiredForUploadError', + ProgrammerIsRequiredForUploadError, + ], + // handle other cli defined errors here +]); export type ServiceError = StatusObject & Error; export namespace ServiceError { export function isCancel(arg: unknown): arg is ServiceError & { code: 1 } { return is(arg) && arg.code === 1; // https://grpc.github.io/grpc/core/md_doc_statuscodes.html } + export function is(arg: unknown): arg is ServiceError { - return arg instanceof Error && isStatusObjet(arg); + return arg instanceof Error && isStatusObject(arg); } - function isStatusObjet(arg: unknown): arg is StatusObject { + + export function isInstanceOf(arg: unknown, type: unknown): boolean { + if (!isStatusObject(arg)) { + return false; + } + + const bin = arg.metadata.get('grpc-status-details-bin')[0]; + + const uint8Array = + typeof bin === 'string' + ? stringToUint8Array(bin) + : new Uint8Array(bin.buffer, bin.byteOffset, bin.byteLength); + + const errors = Status.deserializeBinary(uint8Array) + .getDetailsList() + .map((details) => { + const typeUrl = details.getTypeUrl(); + const ErrorType = protoErrorsMap.get(typeUrl); + return ErrorType?.deserializeBinary(details.getValue_asU8()); + }); + + return !!errors.find((error) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return error && error instanceof type; + }); + } + + function isStatusObject(arg: unknown): arg is StatusObject { if (typeof arg === 'object') { // eslint-disable-next-line @typescript-eslint/no-explicit-any const any = arg as any;