// DownstreamClient 抽象基类(B8 裁决:3 个 BFF 统一使用) // 提供统一的错误映射 + 结构化日志 + metrics 记录 import type { Logger } from "pino"; import { logger } from "../shared/observability/logger.js"; import { BadGatewayError, AggregationFailedError, } from "../shared/errors/application-error.js"; import type { CallContext, DownstreamError, DownstreamResult, DownstreamServiceName, GrpcMetadata, } from "./types.js"; /** gRPC 状态码 → HTTP 语义映射(@grpc/grpc-js status codes) */ const GRPC_STATUS = { OK: 0, CANCELLED: 1, UNKNOWN: 2, INVALID_ARGUMENT: 3, DEADLINE_EXCEEDED: 4, NOT_FOUND: 5, ALREADY_EXISTS: 6, PERMISSION_DENIED: 7, RESOURCE_EXHAUSTED: 8, FAILED_PRECONDITION: 9, ABORTED: 10, OUT_OF_RANGE: 11, UNIMPLEMENTED: 12, INTERNAL: 13, UNAVAILABLE: 14, DATA_LOSS: 15, UNAUTHENTICATED: 16, } as const; export abstract class BaseDownstreamClient { protected log: Logger; abstract readonly serviceName: DownstreamServiceName; constructor() { // abstract property 在子类构造后才可用,延迟初始化 logger this.log = logger.child({ downstream: "downstream" }); } /** 子类在构造函数中调用以初始化 logger(带正确 serviceName) */ protected initLogger(): void { this.log = logger.child({ downstream: this.serviceName }); } /** 构建 gRPC metadata(注入 x-user-id + x-request-id) */ protected buildMetadata(ctx: CallContext): GrpcMetadata { const meta: GrpcMetadata = { "x-user-id": ctx.userId }; if (ctx.traceId) { meta["x-request-id"] = ctx.traceId; } return meta; } /** 将 gRPC 错误映射为 BadGatewayError(502) */ protected mapGrpcError(err: unknown, rpc: string): BadGatewayError { const grpcErr = err as { code?: number; message?: string; details?: string; }; const code = grpcErr.code ?? GRPC_STATUS.UNKNOWN; const message = grpcErr.message ?? "Unknown gRPC error"; this.log.warn( { rpc, grpcCode: code, message, err }, "Downstream gRPC call failed", ); // UNAVAILABLE → 502 Bad Gateway if (code === GRPC_STATUS.UNAVAILABLE) { return new BadGatewayError( `Downstream ${this.serviceName}.${rpc} unavailable`, { service: this.serviceName, rpc, grpcCode: code }, ); } // UNAUTHENTICATED → 映射为 BadGateway(BFF 自身做身份校验,下游不应返回 UNAUTHENTICATED) if (code === GRPC_STATUS.UNAUTHENTICATED) { return new BadGatewayError( `Downstream ${this.serviceName}.${rpc} returned UNAUTHENTICATED`, { service: this.serviceName, rpc, grpcCode: code }, ); } // 其他错误统一映射为 BadGateway return new BadGatewayError( `Downstream ${this.serviceName}.${rpc} failed: ${message}`, { service: this.serviceName, rpc, grpcCode: code, details: grpcErr.details, }, ); } /** 执行 gRPC 调用并统一错误处理 + 超时控制 */ protected async callGrpc( rpc: string, fn: () => Promise, timeoutMs = 3000, ): Promise { const start = Date.now(); try { const result = await Promise.race([ fn(), this.createTimeout(timeoutMs, rpc), ]); const duration = Date.now() - start; this.log.debug( { rpc, durationMs: duration }, "Downstream gRPC call succeeded", ); return result; } catch (err) { throw this.mapGrpcError(err, rpc); } } private createTimeout(ms: number, rpc: string): Promise { return new Promise((_, reject) => { setTimeout(() => { reject( new BadGatewayError( `Downstream ${this.serviceName}.${rpc} timed out after ${ms}ms`, { service: this.serviceName, rpc, timeoutMs: ms }, ), ); }, ms); }); } /** 降级模式 B:部分失败时返回 success=true + degraded=true + warning(president §2.6) */ protected degraded( data: T | null, warning: string, rpc: string, ): DownstreamResult { this.log.warn({ rpc, warning }, "Downstream call degraded"); return { success: true, data, degraded: true, warning }; } /** 聚合失败(多下游部分失败且无降级数据,president §2.6) */ protected aggregationFailed( errors: DownstreamError[], ): AggregationFailedError { return new AggregationFailedError( `Aggregation failed for ${this.serviceName}: ${errors.length} downstream(s) failed`, { service: this.serviceName, errors }, ); } }