import type { HttpExecutionOutcome } from "../../../../adapters/http/http-execution-v3.ts"; import { StudioGatewayError } from "../../application/ports/studio-gateway-error.ts"; import type { ProblemDetails } from "../../contracts/studio/contract.ts"; /** canonical studio-v1.yaml `ProblemDetails.code` enum과 1:1이다. */ export const STUDIO_ERROR_CODES = Object.freeze([ "AUTHENTICATION_REQUIRED", "STUDIO_ACCESS_DENIED", "DOCUMENT_NOT_FOUND", "VERSION_CONFLICT", "REQUEST_VALIDATION_FAILED", "DOCUMENT_VALIDATION_FAILED", "VALIDATION_STALE", "PREVIEW_NOT_FOUND", "PREVIEW_STALE", "PREVIEW_EXPIRED", "PUBLICATION_NOT_FOUND", "PUBLICATION_CONFLICT", "PUBLICATION_EVENT_NOT_FOUND", "PUBLICATION_SNAPSHOT_NOT_FOUND", "WARNING_ACKNOWLEDGEMENT_REQUIRED", "IDEMPOTENCY_KEY_REUSED", "ASSET_NOT_FOUND", "ASSET_NOT_READY", "ASSET_IN_USE", "ASSET_QUARANTINED", "PAYLOAD_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE", "STUDIO_UNAVAILABLE", ]) as readonly ProblemDetails["code"][]; const CODES = new Set(STUDIO_ERROR_CODES); function synthetic( code: ProblemDetails["code"], status: number, detail: string, retryable: boolean, ): StudioGatewayError { return new StudioGatewayError({ type: `https://techlog.local/problems/${code.toLowerCase().replaceAll("_", "-")}`, title: code, status, detail, code, retryable, }); } /** * 서버가 계약 밖 코드를 보내면 도메인 코드를 지어내지 않는다. 전송 계층 * 실패와 마찬가지로 `STUDIO_UNAVAILABLE`로 접는다. */ export function toStudioGatewayError( outcome: HttpExecutionOutcome, operationId: string, ): StudioGatewayError { switch (outcome.kind) { case "PROBLEM": { // 봉투 오류는 wire에 HTTP status를 싣지 않는다 (`envelopeError`가 `status`를 // 0으로 둔다) — 실제 status는 전송 계층이 `outcome.metadata.status`로 // 이미 들고 있으므로 여기서 덮는다. `SafeResponseMetadata.status`는 // `PROBLEM` outcome에서 필수 필드다 (`http-execution-v3.ts`). // `problem`이 falsy이거나 status가 0(봉투의 sentinel)이면 metadata로 // 덮는다 — 원래 코드처럼 `problem`을 안전하지 않게 역참조하지 않는다. const problem = outcome.problem as ProblemDetails | undefined; const status = problem?.status || outcome.metadata.status; if (problem && typeof problem.code === "string" && CODES.has(problem.code)) { return new StudioGatewayError({ ...problem, status }); } return synthetic( "STUDIO_UNAVAILABLE", status, `${operationId} returned an uncontracted problem code.`, false, ); } case "UNAUTHENTICATED": return synthetic("AUTHENTICATION_REQUIRED", 401, `${operationId} requires authentication.`, false); case "FORBIDDEN": return synthetic("STUDIO_ACCESS_DENIED", 403, `${operationId} was denied.`, false); case "CANCELLED": return synthetic("STUDIO_UNAVAILABLE", 499, `${operationId} was cancelled.`, false); case "RATE_LIMITED": return synthetic("STUDIO_UNAVAILABLE", 429, `${operationId} was rate limited.`, true); case "TRANSPORT_FAILURE": return synthetic("STUDIO_UNAVAILABLE", 503, `${operationId} transport failed.`, true); case "AUTH_INTEGRATION_FAILURE": case "CONTRACT_VIOLATION": return synthetic("STUDIO_UNAVAILABLE", 502, `${operationId} broke its contract.`, false); case "SUCCESS": throw new Error(`${operationId}: success outcome is not an error`); } }