"use strict"; /** * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DEPTH_TO_CHECK = exports.preConnectionCodes = exports.genericClasses = exports.redirectCodes = exports.decodeCodes = exports.requestBodyCodes = exports.requestCodes = exports.connectionCodes = void 0; exports.ensureGrpcStatusDetailsParsed = ensureGrpcStatusDetailsParsed; exports.resolveErrorInfoReason = resolveErrorInfoReason; exports.resolveServerErrorCode = resolveServerErrorCode; exports.resolveClientNetworkOrOperationalError = resolveClientNetworkOrOperationalError; exports.resolveLanguageSpecificErrorType = resolveLanguageSpecificErrorType; exports.resolveErrorType = resolveErrorType; exports.resolveExceptionType = resolveExceptionType; exports.resolveSystemErrorCode = resolveSystemErrorCode; exports.resolveRpcStatusName = resolveRpcStatusName; exports.resolveHttpStatusCode = resolveHttpStatusCode; exports.isPreConnectionFailure = isPreConnectionFailure; exports.resolveErrorMessage = resolveErrorMessage; exports.isServerSideError = isServerSideError; exports.safeJsonStringify = safeJsonStringify; exports.resolveServerExceptionDetails = resolveServerExceptionDetails; exports.recordExceptionEvent = recordExceptionEvent; const googleError_1 = require("../googleError"); const status_1 = require("../status"); /** * System and network error codes mapped to CLIENT_CONNECTION_ERROR. */ exports.connectionCodes = [ 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ENETDOWN', 'EPIPE', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET', 'CERT_HAS_EXPIRED', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN', 'ERR_TLS_CERT_ALTNAME_INVALID', 'UNABLE_TO_GET_ISSUER_CERT', ]; /** * System and client request error codes mapped to CLIENT_REQUEST_ERROR. */ exports.requestCodes = [ 'ERR_INVALID_ARG_TYPE', 'ERR_INVALID_URL', 'ERR_HTTP_INVALID_HEADER_VALUE', 'ERR_INVALID_HTTP_TOKEN', 'ERR_INVALID_PROTOCOL', 'ERR_INVALID_ARG_VALUE', ]; /** * Stream and body error codes mapped to CLIENT_REQUEST_BODY_ERROR. */ exports.requestBodyCodes = [ 'ERR_STREAM_WRITE_AFTER_END', 'ERR_STREAM_DESTROYED', 'ERR_STREAM_ALREADY_FINISHED', 'ERR_STREAM_CANNOT_PIPE', 'ERR_STREAM_NULL_VALUES', 'ERR_STREAM_PREMATURE_CLOSE', ]; /** * Buffer error codes mapped to CLIENT_RESPONSE_DECODE_ERROR. */ exports.decodeCodes = ['ERR_BUFFER_OUT_OF_BOUNDS']; /** * Redirect error codes mapped to CLIENT_REDIRECT_ERROR. */ exports.redirectCodes = [ 'ERR_TOO_MANY_REDIRECTS', 'ERR_FR_TOO_MANY_REDIRECTS', ]; /** * Generic class / constructor names that should be unwrapped to find more specific error causes. */ exports.genericClasses = [ 'Error', 'GoogleError', 'Object', 'DOMException', ]; /** * Error codes indicating failures prior to network connection establishment. */ exports.preConnectionCodes = [ 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'ERR_INVALID_ARG_TYPE', 'ERR_INVALID_URL', ]; /** * Maximum traversal depth for unwrapping nested error causes. */ exports.DEPTH_TO_CHECK = 10; /** * Decodes binary gRPC status details (`grpc-status-details-bin`) onto the error * object if present and not yet parsed. * * @param {unknown} e - The error to inspect and populate with parsed gRPC status details. */ function ensureGrpcStatusDetailsParsed(e) { if (!e || typeof e !== 'object') { return; } const errWithMeta = e; if (!errWithMeta.reason && !errWithMeta.statusDetails && errWithMeta.metadata && typeof errWithMeta.metadata.get === 'function' && errWithMeta.metadata.get('grpc-status-details-bin')?.length > 0) { try { googleError_1.GoogleError.parseGRPCStatusDetails(errWithMeta); } catch { // Ignore decoding errors. } } } /** * Resolves google.rpc.ErrorInfo reason if present on the error or its cause chain. * Corresponds to Tier 1 in the error.type hierarchy. * * @param {unknown} e - The error to inspect. * @returns {string | undefined} The ErrorInfo reason string if found, or undefined. */ function resolveErrorInfoReason(e) { ensureGrpcStatusDetailsParsed(e); let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } // Guard against circular cause references. if (seen.has(current)) { break; } seen.add(current); const err = current; // Check direct reason property on error. if (typeof err.reason === 'string' && err.reason.length > 0) { return err.reason; } // Check errorInfo object on error. if (err.errorInfo && typeof err.errorInfo === 'object') { const infoReason = err.errorInfo.reason; if (typeof infoReason === 'string' && infoReason.length > 0) { return infoReason; } } // Inspect statusDetails array for reason or errorInfo. if (Array.isArray(err.statusDetails)) { for (const detail of err.statusDetails) { if (detail && typeof detail === 'object') { if ('reason' in detail && typeof detail.reason === 'string' && detail.reason.length > 0) { return detail.reason; } if ('errorInfo' in detail && detail.errorInfo && typeof detail.errorInfo === 'object' && typeof detail.errorInfo.reason === 'string' && detail.errorInfo.reason.length > 0) { return detail.errorInfo.reason; } } } } // Traverse error cause chain. current = err.cause; } return undefined; } /** * Resolves a server error code received from the backend service: * - For HTTP: The HTTP status code string (e.g. '400', '403', '503'). * - For gRPC: The canonical gRPC status code name in uppercase (e.g. 'PERMISSION_DENIED', 'UNAVAILABLE'). * Corresponds to Tier 2 in the error.type hierarchy. * * @param {unknown} e - The error to inspect. * @param {'grpc' | 'http'} rpcType - The RPC transport protocol. * @returns {string | undefined} The server error code string if the error came from the server, or undefined. */ function resolveServerErrorCode(e, rpcType) { if (rpcType === 'http') { const httpStatus = resolveHttpStatusCode(e); return httpStatus !== undefined ? httpStatus.toString() : undefined; } if (rpcType === 'grpc') { if (isPreConnectionFailure(e) || resolveClientNetworkOrOperationalError(e) !== undefined) { return undefined; } // Exclude client-side aborts across the cause chain (timeouts are already // matched by resolveClientNetworkOrOperationalError above). let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object' || seen.has(current)) { break; } seen.add(current); const err = current; if (err.name === 'AbortError') { return undefined; } current = err.cause; } return resolveRpcStatusName(e); } return undefined; } /** * Resolves client-side network and operational errors to standard CLIENT_* identifiers. * Corresponds to Tier 3 in the error.type hierarchy. * * @param {unknown} e - The error to inspect. * @returns {string | undefined} The standardized CLIENT_* error type if matched, or undefined. */ function resolveClientNetworkOrOperationalError(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } if (seen.has(current)) { break; } seen.add(current); const err = current; const name = typeof err.name === 'string' ? err.name : undefined; const constructorName = err.constructor?.name; const code = typeof err.code === 'string' ? err.code : undefined; const message = typeof err.message === 'string' ? err.message : ''; // 1. CLIENT_TIMEOUT if (name === 'TimeoutError' || code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT' || /timeout.*exceeded|deadline.*exceeded|total timeout/i.test(message)) { return 'CLIENT_TIMEOUT'; } // 2. CLIENT_CONNECTION_ERROR if ((code && exports.connectionCodes.includes(code)) || (code && code.startsWith('ERR_SSL')) || name === 'TLSError') { return 'CLIENT_CONNECTION_ERROR'; } // 3. CLIENT_REQUEST_ERROR if ((code && exports.requestCodes.includes(code)) || name === 'URIError' || constructorName === 'URIError') { return 'CLIENT_REQUEST_ERROR'; } // 4. CLIENT_REQUEST_BODY_ERROR if (code && exports.requestBodyCodes.includes(code)) { return 'CLIENT_REQUEST_BODY_ERROR'; } // 5. CLIENT_RESPONSE_DECODE_ERROR if ((code && exports.decodeCodes.includes(code)) || name === 'DecodeError' || constructorName === 'DecodeError' || name === 'SyntaxError' || constructorName === 'SyntaxError') { return 'CLIENT_RESPONSE_DECODE_ERROR'; } // 6. CLIENT_REDIRECT_ERROR if (code && exports.redirectCodes.includes(code)) { return 'CLIENT_REDIRECT_ERROR'; } // 7. CLIENT_AUTHENTICATION_ERROR if (name === 'GoogleAuthError' || constructorName === 'GoogleAuthError' || code === 'ERR_NO_CREDENTIALS' || code === 'MISSING_CREDENTIALS') { return 'CLIENT_AUTHENTICATION_ERROR'; } current = err.cause; } return undefined; } /** * Resolves a language-specific error type name (e.g. AbortError, TypeError, RangeError, CustomRpcError). * Generic wrapper types (Error, GoogleError, Object, DOMException) are excluded and unwrap e.cause. * Corresponds to Tier 4 in the error.type hierarchy. * * @param {unknown} e - The error to inspect. * @returns {string | undefined} The language-specific error class or name if found, or undefined. */ function resolveLanguageSpecificErrorType(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } // Guard against circular cause references. if (seen.has(current)) { break; } seen.add(current); const err = current; // Prioritize AbortError regardless of class hierarchy. if (err.name === 'AbortError') { return 'AbortError'; } // Prefer specific constructor class name over generic base wrappers. const className = err.constructor?.name; if (className && !exports.genericClasses.includes(className)) { return className; } // Fall back to non-generic error name if available. if (typeof err.name === 'string' && err.name.length > 0 && !exports.genericClasses.includes(err.name)) { return err.name; } // Unwrap cause chain when encountering generic wrapper classes. current = err.cause; } return undefined; } /** * Resolves the OpenTelemetry `error.type` attribute according to the 5-tier hierarchy: * 1. google.rpc.ErrorInfo.reason * 2. Specific Server Error Code (HTTP status code or gRPC status name) * 3. Client-Side Network/Operational Errors (CLIENT_* standardized strings) * 4. Language-specific error type (e.g. AbortError, RangeError, TypeError, CustomRpcError) * 5. Internal Fallback ("INTERNAL") * * @param {unknown} e - The error to classify. * @param {'grpc' | 'http'} rpcType - The RPC transport protocol. * @returns {string} The resolved `error.type` value. */ function resolveErrorType(e, rpcType) { // Tier 1: google.rpc.ErrorInfo.reason const errorInfoReason = resolveErrorInfoReason(e); if (errorInfoReason) { return errorInfoReason; } // Tier 2: Specific Server Error Code const serverErrorCode = resolveServerErrorCode(e, rpcType); if (serverErrorCode) { return serverErrorCode; } // Tier 3: Client-Side Network/Operational Errors const clientError = resolveClientNetworkOrOperationalError(e); if (clientError) { return clientError; } // Tier 4: Language-specific error type const languageError = resolveLanguageSpecificErrorType(e); if (languageError) { return languageError; } // Tier 5: Internal Fallback return 'INTERNAL'; } /** * Resolves the exception type name for a failed call. Prefers specific * exception names (e.g. `AbortError`, `TimeoutError`, `TypeError`) and * constructor names over generic `Error`, falling back to `e.name` when * the constructor is generic or `DOMException`. * * @param {Error} e - The Error instance. * @returns {string} The resolved `exception.type` string. */ function resolveExceptionType(e) { if (e.name && (e.name === 'AbortError' || e.name === 'TimeoutError')) { return e.name; } const className = e.constructor?.name; if (className && className !== 'Error' && className !== 'Object' && className !== 'DOMException') { return className; } return e.name || 'Error'; } /** * Resolves a Node system error code (e.g. `ECONNREFUSED`), checking the error * itself and any underlying cause attached by fallback wrapping. * * @param {unknown} e - The error to inspect. * @returns {string | undefined} The system error code string if present, or undefined. */ function resolveSystemErrorCode(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } if (seen.has(current)) { break; } seen.add(current); const code = current.code; if (typeof code === 'string' && code.length > 0) { return code; } current = current.cause; } return undefined; } /** * Resolves the canonical gRPC status name for a failed call. Status 0 (OK) * and codes outside the `Status` enum are treated as absent for failed calls. * * @param {unknown} e - The error to inspect. * @returns {string | undefined} The canonical gRPC status name if present, or undefined. */ function resolveRpcStatusName(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } if (seen.has(current)) { break; } seen.add(current); const code = current.code; if (typeof code === 'number' && code !== status_1.Status.OK && status_1.Status[code] !== undefined) { return status_1.Status[code]; } current = current.cause; } return undefined; } /** * Reads the HTTP response status recorded on a fallback error. * * @param {unknown} e - The error to inspect. * @returns {number | undefined} The numeric HTTP status code if present, or undefined. */ function resolveHttpStatusCode(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { if (!current || typeof current !== 'object') { break; } if (seen.has(current)) { break; } seen.add(current); const code = current.httpStatusCode; if (typeof code === 'number') { return code; } current = current.cause; } return undefined; } /** * Determines if a failure occurred on the client side before DNS resolution * or connection establishment. * * @param {unknown} e - The error to inspect. * @returns {boolean} True if the error occurred before connection establishment. */ function isPreConnectionFailure(e) { let current = e; const seen = new Set(); for (let depth = 0; depth < exports.DEPTH_TO_CHECK; depth++) { // Server status code indicates a response was received. if (resolveHttpStatusCode(current) !== undefined || resolveRpcStatusName(current) !== undefined) { return false; } // Non-Error throws or objects without stack are client failures. if (!current || !(current instanceof Error || (typeof current === 'object' && 'stack' in current))) { return true; } // Guard against circular cause references. if (seen.has(current)) { return false; } seen.add(current); // Client-side validation errors happen before connection. const err = current; if (current instanceof TypeError || current instanceof RangeError || current instanceof URIError || err.name === 'TypeError' || err.name === 'RangeError' || err.name === 'URIError') { return true; } // Check for network error codes occurring before connection establishment. const systemCode = resolveSystemErrorCode(current); if (systemCode && exports.preConnectionCodes.includes(systemCode)) { return true; } // Unwrap GoogleError wrappers to inspect underlying cause. if ((current instanceof googleError_1.GoogleError || current.constructor?.name === 'GoogleError') && err.cause) { current = err.cause; } else { return false; } } return false; } /** * Resolves a human-readable description for non-Error throws, extracting * `message` if present or falling back to `String(e)`. * * @param {unknown} e - The non-Error value thrown or reported. * @returns {string} The resolved error message string. */ function resolveErrorMessage(e) { const message = e?.message; return typeof message === 'string' ? message : String(e); } /** * Determines whether a failure is a server-side error (i.e. a server response arrived), * excluding pre-connection failures, client network/operational errors, and aborts/timeouts. * * @param {unknown} e - The error to inspect. * @param {'grpc' | 'http'} rpcType - The RPC transport protocol. * @returns {boolean} True if a server error code was resolved for the transport. */ function isServerSideError(e, rpcType) { return resolveServerErrorCode(e, rpcType) !== undefined; } /** * Checks whether a value is a Node.js Buffer instance. * * @param {unknown} val - The value to check. * @returns {boolean} True if `val` is a Buffer. */ function isBuffer(val) { return typeof Buffer !== 'undefined' && Buffer.isBuffer(val); } /** * Safely converts a value to a JSON string without throwing exceptions on * circular references, BigInt values, or non-serializable properties. * * @param {unknown} value - The value to serialize. * @returns {string | undefined} The serialized JSON string, or undefined if `value` is undefined or unserializable. */ function safeJsonStringify(value) { if (value === undefined) { return undefined; } try { const ancestors = []; return JSON.stringify(value, function (_key, val) { if (typeof val === 'bigint') { return val.toString(); } if (typeof val !== 'object' || val === null) { return val; } // Track object ancestry to replace circular references. if (ancestors.includes(this)) { while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) { ancestors.pop(); } } if (ancestors.includes(val)) { return '[Circular]'; } ancestors.push(val); return val; }); } catch { try { return String(value); } catch { return undefined; } } } /** * Extracts and formats server-side error details (message, status details, and metadata) * attached by GFE/backend on server-side errors. Note that only `message` is used on * low level network `exception` span events (`exception.stacktrace` is not recorded on spans). * * @param {Error} e - The server Error instance. * @returns {{message: string; stacktrace?: string}} The resolved server message and formatted details string. */ function resolveServerExceptionDetails(e) { ensureGrpcStatusDetailsParsed(e); const errObj = e; const causeObj = errObj.cause && typeof errObj.cause === 'object' ? errObj.cause : undefined; // Prefer server error details over generic error message. const serverDetails = errObj.details ?? causeObj?.details; const message = typeof serverDetails === 'string' && serverDetails.length > 0 ? serverDetails : e.message; // Serialize status details if present. const rawStatusDetails = errObj.statusDetails ?? causeObj?.statusDetails; let statusDetailsStr; if (rawStatusDetails !== undefined && rawStatusDetails !== null) { statusDetailsStr = typeof rawStatusDetails === 'string' ? rawStatusDetails : safeJsonStringify(rawStatusDetails); } // Serialize backend metadata, encoding Buffer values as base64. const rawMetadata = errObj.metadata ?? causeObj?.metadata; let metadataStr; if (rawMetadata && typeof rawMetadata === 'object') { try { let map; if (typeof rawMetadata.getMap === 'function') { map = rawMetadata.getMap(); } else { map = rawMetadata; } const cleanMap = {}; for (const [key, val] of Object.entries(map)) { if (isBuffer(val)) { cleanMap[key] = val.toString('base64'); } else if (Array.isArray(val)) { cleanMap[key] = val.map(item => isBuffer(item) ? item.toString('base64') : item); } else { cleanMap[key] = val; } } metadataStr = safeJsonStringify(cleanMap); } catch { metadataStr = safeJsonStringify(rawMetadata); } } // Combine status details and metadata into a formatted stacktrace string. const stacktraceParts = []; if (statusDetailsStr) { stacktraceParts.push(`status_details: ${statusDetailsStr}`); } if (metadataStr) { stacktraceParts.push(`metadata: ${metadataStr}`); } const stacktrace = stacktraceParts.length > 0 ? stacktraceParts.join('\n') : undefined; return { message, stacktrace, }; } /** * Records an `exception` span event with `exception.type` (and `exception.message` * when `includeMessage` is true, such as on low level network attempt spans). * * @param {Span} span - The span on which to record the exception event. * @param {Error} e - The error instance. * @param {'grpc' | 'http'} rpcType - The RPC transport protocol. * @param {boolean} [includeMessage=false] - Whether to include `exception.message` on the event * (true for low level network attempt spans, false for client request spans). */ function recordExceptionEvent(span, e, rpcType, includeMessage = false) { const attributes = { 'exception.type': resolveExceptionType(e), }; if (includeMessage) { attributes['exception.message'] = isServerSideError(e, rpcType) ? resolveServerExceptionDetails(e).message : e.message; } span.addEvent('exception', attributes); } //# sourceMappingURL=errorUtil.js.map