diff --git a/packages/core/src/utils/errors.ts b/packages/core/src/utils/errors.ts index dc7d5b33c5..5465977ff2 100644 --- a/packages/core/src/utils/errors.ts +++ b/packages/core/src/utils/errors.ts @@ -10,6 +10,16 @@ interface GaxiosError { }; } +function isGaxiosError(error: unknown): error is GaxiosError { + return ( + typeof error === 'object' && + error !== null && + 'response' in error && + typeof (error as { response: unknown }).response === 'object' && + (error as { response: unknown }).response !== null + ); +} + export function isNodeError(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && 'code' in error; } @@ -105,11 +115,41 @@ interface ResponseData { }; } +function isResponseData(data: unknown): data is ResponseData { + if (typeof data !== 'object' || data === null) { + return false; + } + const candidate = data as ResponseData; + if (!('error' in candidate)) { + return false; + } + const error = candidate.error; + if (typeof error !== 'object' || error === null) { + return false; // error property exists but is not an object (could be undefined, but we checked 'in') + } + + // Optional properties check + if ( + 'code' in error && + typeof error.code !== 'number' && + error.code !== undefined + ) { + return false; + } + if ( + 'message' in error && + typeof error.message !== 'string' && + error.message !== undefined + ) { + return false; + } + + return true; +} + export function toFriendlyError(error: unknown): unknown { - if (error && typeof error === 'object' && 'response' in error) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - const gaxiosError = error as GaxiosError; - const data = parseResponseData(gaxiosError); + if (isGaxiosError(error)) { + const data = parseResponseData(error); if (data && data.error && data.error.message && data.error.code) { switch (data.error.code) { case 400: @@ -129,17 +169,20 @@ export function toFriendlyError(error: unknown): unknown { } function parseResponseData(error: GaxiosError): ResponseData | undefined { + let data = error.response?.data; // Inexplicably, Gaxios sometimes doesn't JSONify the response data. - if (typeof error.response?.data === 'string') { + if (typeof data === 'string') { try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return JSON.parse(error.response?.data) as ResponseData; + data = JSON.parse(data); } catch { return undefined; } } - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return error.response?.data as ResponseData | undefined; + + if (isResponseData(data)) { + return data; + } + return undefined; } /** @@ -152,8 +195,15 @@ function parseResponseData(error: GaxiosError): ResponseData | undefined { export function isAuthenticationError(error: unknown): boolean { // Check for MCP SDK errors with code property // (SseError and StreamableHTTPError both have numeric 'code' property) - if (error && typeof error === 'object' && 'code' in error) { - const errorCode = (error as { code: unknown }).code; + if ( + error && + typeof error === 'object' && + 'code' in error && + typeof (error as { code: unknown }).code === 'number' + ) { + // Safe access after check + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const errorCode = (error as { code: number }).code; if (errorCode === 401) { return true; } diff --git a/packages/core/src/utils/googleErrors.ts b/packages/core/src/utils/googleErrors.ts index 3ae74f0de8..88f78e35d6 100644 --- a/packages/core/src/utils/googleErrors.ts +++ b/packages/core/src/utils/googleErrors.ts @@ -207,9 +207,13 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { detailObj['@type'] = detailObj[typeKey]; delete detailObj[typeKey]; } - // We can just cast it; the consumer will have to switch on @type - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - details.push(detailObj as unknown as GoogleApiErrorDetail); + // Basic structural check before casting. + // Since the proto definitions are loose, we primarily rely on @type presence. + if (typeof detailObj['@type'] === 'string') { + // We can just cast it; the consumer will have to switch on @type + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + details.push(detailObj as unknown as GoogleApiErrorDetail); + } } } } @@ -225,6 +229,16 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null { return null; } +function isErrorShape(obj: unknown): obj is ErrorShape { + return ( + typeof obj === 'object' && + obj !== null && + (('message' in obj && + typeof (obj as { message: unknown }).message === 'string') || + ('code' in obj && typeof (obj as { code: unknown }).code === 'number')) + ); +} + function fromGaxiosError(errorObj: object): ErrorShape | undefined { const gaxiosError = errorObj as { response?: { @@ -260,7 +274,10 @@ function fromGaxiosError(errorObj: object): ErrorShape | undefined { if (typeof data === 'object' && data !== null) { if ('error' in data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - outerError = (data as { error: ErrorShape }).error; + const potentialError = (data as { error: unknown }).error; + if (isErrorShape(potentialError)) { + outerError = potentialError; + } } } } @@ -320,7 +337,10 @@ function fromApiError(errorObj: object): ErrorShape | undefined { if (typeof data === 'object' && data !== null) { if ('error' in data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - outerError = (data as { error: ErrorShape }).error; + const potentialError = (data as { error: unknown }).error; + if (isErrorShape(potentialError)) { + outerError = potentialError; + } } } }