The public site has a Releases page and a footer link to the latest release, and both were empty — the read side has existed since the public surface landed, but nothing could ever create a row. The seven release operations were in the contract with no implementation, so the changelog was a page that could only ever be blank. The release model is not a blob of prose. It splits into six markdown sections because a release note answers fixed questions — why, what, what changes for a reader, what it leaves in the code, how it was verified, what is still missing — and a single text column cannot say which of those went unanswered. Publishing is the only thing that makes a release public: the public query filters on `workflow_status = 'PUBLISHED'` and nothing else. So publish is where the contract's required fields are actually enforced. Saving stays permissive — a draft you cannot save until it is complete is a draft you cannot write — and the two demands are deliberately different. `version_label` is NOT NULL UNIQUE but a draft has no version yet, so creation writes a placeholder derived from the row id and publication refuses to ship one. Relaxing the column instead would open a window where a published release is publicly visible with no version at all. A published release cannot be deleted, only archived: a public changelog entry that vanishes leaves everyone who linked it with no way to learn what happened. Also registers `adapter-outbound-objectstorage` as an app-bootstrap runtime member. It was added as a dependency when asset upload was fixed but never registered, and `verifyRuntimeModuleMembership` had not been run since.
1405 lines
58 KiB
YAML
1405 lines
58 KiB
YAML
# Registry: Error Codes
|
|
# SSOT: wiki/projects/ca-tmpl/registries/error-codes.yaml
|
|
# Schema owner: feature-contract-registry-governance
|
|
# Category enum owner: feature-operational-error-observability-foundation
|
|
# Last updated: 2026-05-22
|
|
# Note: 이 파일은 Phase B 산출물. Phase C2(ca-tmpl 실 코드)에서 generated Java constants의 source.
|
|
#
|
|
# Schema (per row):
|
|
# code: UPPER_SNAKE_CASE
|
|
# category: VALIDATION | AUTH | AUTHZ | NOT_FOUND | CONFLICT |
|
|
# RATE_LIMIT | TRANSIENT_DEPENDENCY | PERMANENT_DEPENDENCY |
|
|
# DATA_INTEGRITY | INTERNAL
|
|
# http_status: int (async-only failures use 500 placeholder)
|
|
# retryable: bool
|
|
# retry_after_seconds: int | null (RATE_LIMIT/TRANSIENT 권고 backoff)
|
|
# owner_branch: source branch (raw/branch-notes/feature-*.md)
|
|
# owner_layer: presentation | application | domain | infrastructure | crosscut
|
|
# client_safe_message: no token / no principal raw / no internal path / no stack trace
|
|
# log_level: ERROR | WARN | INFO
|
|
# runbook_link: runbook://area/scenario OR null (client-error만 null 허용)
|
|
# compatibility_impact: none | additive | behavior-change | breaking
|
|
# required_test: owning contract test identifier
|
|
#
|
|
# Runbook policy (operational-runbook-contract L80):
|
|
# retryable=false + category ∈ {AUTH, AUTHZ, RATE_LIMIT, INTERNAL,
|
|
# TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY} ⇒ runbook_link 필수.
|
|
# VALIDATION/NOT_FOUND/CONFLICT/DATA_INTEGRITY는 client-error로 runbook 면제 가능.
|
|
# retryable=true 인 모든 row는 runbook_link 필수.
|
|
|
|
errors:
|
|
|
|
# ============================================================
|
|
# AUTH (feature-security-operational-baseline / Decision Matrix)
|
|
# ============================================================
|
|
|
|
# source: feature-security-operational-baseline L82 — "token 누락 | 401 | AUTH_TOKEN_MISSING | AUTH"
|
|
- code: AUTH_TOKEN_MISSING
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication required"
|
|
log_level: WARN
|
|
runbook_link: "runbook://auth/token-missing"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L83 — "token malformed (parse fail) | 401 | AUTH_TOKEN_MALFORMED | AUTH"
|
|
- code: AUTH_TOKEN_MALFORMED
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed"
|
|
log_level: WARN
|
|
runbook_link: "runbook://auth/token-malformed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L84 — "token expired (clock skew tolerance 60s 초과) | 401 | AUTH_TOKEN_EXPIRED | AUTH"
|
|
- code: AUTH_TOKEN_EXPIRED
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication expired"
|
|
log_level: WARN
|
|
runbook_link: "runbook://auth/token-expired"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L85 — "invalid signature | 401 | AUTH_TOKEN_INVALID_SIGNATURE | AUTH"
|
|
- code: AUTH_TOKEN_INVALID_SIGNATURE
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/token-invalid-signature"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L86 — "issuer mismatch | 401 | AUTH_ISSUER_MISMATCH | AUTH"
|
|
- code: AUTH_ISSUER_MISMATCH
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/issuer-mismatch"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L87 — "audience mismatch | 401 | AUTH_AUDIENCE_MISMATCH | AUTH"
|
|
- code: AUTH_AUDIENCE_MISMATCH
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/audience-mismatch"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L88 — "unknown kid (JWKS 미캐시) | 401 + Retry-After 5s | AUTH_KID_UNKNOWN | AUTH"
|
|
- code: AUTH_KID_UNKNOWN
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: true # 2026-06-01: false→true. JWKS 키 회전 중 unknown kid 는 ~5s 후 JWKS refresh 로 해소 가능(transient). retry_after_seconds=5 + client_safe_message "please retry" 와 정합. 키 고정 정책으로 전환 시 false 복귀.
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed, please retry"
|
|
log_level: WARN
|
|
runbook_link: "runbook://auth/kid-unknown"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L89 — "JWKS endpoint outage ... | AUTH_JWKS_UNAVAILABLE | TRANSIENT_DEPENDENCY"
|
|
- code: AUTH_JWKS_UNAVAILABLE
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 30
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Authentication service temporarily unavailable"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/jwks-unavailable"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-security-operational-baseline L90 — "claim mapping failure ... | 401 | AUTH_CLAIM_MAPPING_FAILED | AUTH"
|
|
- code: AUTH_CLAIM_MAPPING_FAILED
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Authentication failed"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/claim-mapping-failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# ============================================================
|
|
# AUTHZ (feature-security-operational-baseline)
|
|
# ============================================================
|
|
|
|
# source: feature-security-operational-baseline L91 — "valid token + 권한 부족 | 403 | AUTHZ_INSUFFICIENT_PERMISSION | AUTHZ"
|
|
- code: AUTHZ_INSUFFICIENT_PERMISSION
|
|
category: AUTHZ
|
|
http_status: 403
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Permission denied"
|
|
log_level: WARN
|
|
runbook_link: "runbook://authz/insufficient-permission"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:authz-category
|
|
|
|
# source: feature-security-operational-baseline L92 — "valid token + tenant cross-access | 403 | AUTHZ_TENANT_MISMATCH | AUTHZ"
|
|
- code: AUTHZ_TENANT_MISMATCH
|
|
category: AUTHZ
|
|
http_status: 403
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Permission denied"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://authz/tenant-mismatch"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:authz-category
|
|
|
|
# ============================================================
|
|
# INTERNAL (feature-security-operational-baseline + container-runtime)
|
|
# ============================================================
|
|
|
|
# source: feature-security-operational-baseline L93 — "public path misconfiguration ... | 500 + P1 alert | INTERNAL_AUTH_MISCONFIGURATION | INTERNAL"
|
|
- code: INTERNAL_AUTH_MISCONFIGURATION
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-security-operational-baseline
|
|
owner_layer: crosscut
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://auth/public-path-misconfiguration"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:auth-category
|
|
|
|
# source: feature-container-runtime-contract L113 — "JVM OutOfMemoryError → ExitOnOutOfMemoryError로 137 exit, log에 error.code=JVM_OOM 명시"
|
|
- code: JVM_OOM
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-container-runtime-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://runtime/jvm-oom"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:container-runtime-oom
|
|
|
|
# ============================================================
|
|
# DB / Persistence (feature-persistence-failure-baseline / SQLState Matrix)
|
|
# ============================================================
|
|
|
|
# source: feature-persistence-failure-baseline L85 — "08* | all | TRANSIENT_DEPENDENCY | DB_UNAVAILABLE | true"
|
|
- code: DB_UNAVAILABLE
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Service temporarily unavailable"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://db/unavailable"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L86 — "40001 | Postgres/MySQL | CONFLICT | DB_SERIALIZATION_FAILURE | true"
|
|
- code: DB_SERIALIZATION_FAILURE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: true
|
|
retry_after_seconds: 1
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request conflicted with another transaction, please retry"
|
|
log_level: WARN
|
|
runbook_link: "runbook://db/serialization-failure"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L87 — "40P01 | Postgres | CONFLICT | DB_DEADLOCK | true (backoff)"
|
|
- code: DB_DEADLOCK
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: true
|
|
retry_after_seconds: 1
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request conflicted, please retry"
|
|
log_level: WARN
|
|
runbook_link: "runbook://db/deadlock"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L88 — "23502 | Postgres | DATA_INTEGRITY | DB_NULL_VIOLATION | false"
|
|
- code: DB_NULL_VIOLATION
|
|
category: DATA_INTEGRITY
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request violates a required field constraint"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L89 — "23503 | Postgres | DATA_INTEGRITY | DB_FK_VIOLATION | false"
|
|
- code: DB_FK_VIOLATION
|
|
category: DATA_INTEGRITY
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request references missing resource"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L90 — "23505 | Postgres | CONFLICT | DB_UNIQUE_VIOLATION | false (business mapping)"
|
|
- code: DB_UNIQUE_VIOLATION
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Resource already exists"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L91 — "23514 | Postgres | DATA_INTEGRITY | DB_CHECK_VIOLATION | false"
|
|
- code: DB_CHECK_VIOLATION
|
|
category: DATA_INTEGRITY
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request violates a value constraint"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L92 — "25P03 | Postgres | TRANSIENT_DEPENDENCY | DB_IDLE_IN_TX_TIMEOUT | true"
|
|
- code: DB_IDLE_IN_TX_TIMEOUT
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 2
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Service temporarily unavailable"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://db/idle-in-tx-timeout"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# source: feature-persistence-failure-baseline L93 — "57014 | Postgres | TRANSIENT_DEPENDENCY | DB_QUERY_CANCELED | false"
|
|
- code: DB_QUERY_CANCELED
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-persistence-failure-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Request was canceled, please retry later"
|
|
log_level: WARN
|
|
runbook_link: "runbook://db/query-canceled"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:persistence-mapping
|
|
|
|
# ============================================================
|
|
# Rate limit / Idempotency (feature-rate-limit-idempotency-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-rate-limit-idempotency-contract — rate limit response/log 기준 / Retry-After header 기준 (scope L29, L33)
|
|
- code: RATE_LIMIT_EXCEEDED
|
|
category: RATE_LIMIT
|
|
http_status: 429
|
|
retryable: true
|
|
retry_after_seconds: 1
|
|
owner_branch: feature-rate-limit-idempotency-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Too many requests, please retry after the indicated interval"
|
|
log_level: WARN
|
|
runbook_link: "runbook://rate-limit/exceeded"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:rate-limit
|
|
|
|
# source: feature-rate-limit-idempotency-contract L71 — "200ms 초과 시 409 IDEMPOTENT_IN_FLIGHT (retryable=false, client는 polling)"
|
|
- code: IDEMPOTENT_IN_FLIGHT
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-rate-limit-idempotency-contract
|
|
owner_layer: application
|
|
client_safe_message: "A previous identical request is still being processed, please poll for result"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:idempotency
|
|
|
|
# source: feature-rate-limit-idempotency-contract L72 — "fingerprint mismatch (same key + different body) = 422 IDEMPOTENT_REQUEST_MISMATCH"
|
|
- code: IDEMPOTENT_REQUEST_MISMATCH
|
|
category: VALIDATION
|
|
http_status: 422
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-rate-limit-idempotency-contract
|
|
owner_layer: application
|
|
client_safe_message: "Idempotency key reused with different request body"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:idempotency
|
|
|
|
# ============================================================
|
|
# File / Resource (feature-file-resource-handling-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-file-resource-handling-contract L69 — "spring.servlet.multipart.max-file-size 10MB ... Spring 단의 enforcement가 실패 시 envelope 응답 보장" / 테스트 계약 "oversized upload가 generic 500으로 처리되면 실패"
|
|
- code: UPLOAD_SIZE_EXCEEDED
|
|
category: VALIDATION
|
|
http_status: 413
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-file-resource-handling-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Uploaded file exceeds maximum size"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:file-upload
|
|
|
|
# source: feature-file-resource-handling-contract L72 — "allowed content-type allowlist starting set ..."
|
|
- code: UPLOAD_CONTENT_TYPE_REJECTED
|
|
category: VALIDATION
|
|
http_status: 415
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-file-resource-handling-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Uploaded content type is not allowed"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:file-upload
|
|
|
|
# source: feature-file-resource-handling-contract — Decisionized Work Items "path traversal | normalized storage key only ... | traversal test"
|
|
- code: PATH_TRAVERSAL_DETECTED
|
|
category: VALIDATION
|
|
http_status: 400
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-file-resource-handling-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Invalid file path"
|
|
log_level: ERROR
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:file-upload
|
|
|
|
# source: feature-file-resource-handling-contract L73 — "streaming download backpressure = response timeout 60s, max stream 100MB. 초과 시 truncate + ERROR log"
|
|
- code: DOWNLOAD_STREAMING_FAILURE
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-file-resource-handling-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Download failed, please retry"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://file/download-streaming-failure"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:file-download
|
|
|
|
# ============================================================
|
|
# API contract transport-standard codes (feature-api-contract-baseline)
|
|
# ============================================================
|
|
# NOTE: feature-api-contract-baseline owns the transport-shape failure
|
|
# classification (D8 413/414, D9 406/415, D12 405, D15 412). These rows mirror
|
|
# dev.caskeleton.shared.error.OperationalError; the D11 status-mapping
|
|
# consistency test (owner: this branch, producer) fails the build when a code's
|
|
# registry http_status and the enum httpStatus() drift apart.
|
|
|
|
# source: feature-api-contract-baseline.md D12 — "405 Method Not Allowed + Allow header 의무"
|
|
- code: METHOD_NOT_ALLOWED
|
|
category: VALIDATION
|
|
http_status: 405
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "HTTP method not allowed for this resource"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# source: feature-api-contract-baseline.md D9 — "406 Not Acceptable = 응답 표현 협상 실패"
|
|
- code: NOT_ACCEPTABLE
|
|
category: VALIDATION
|
|
http_status: 406
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "No acceptable representation for the requested Accept header"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# source: feature-api-contract-baseline.md D15 — "If-Match mismatch 시 412 Precondition Failed"
|
|
- code: PRECONDITION_FAILED
|
|
category: CONFLICT
|
|
http_status: 412
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Resource was modified by another request; refetch and retry"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# source: feature-api-contract-baseline.md D8 — "request size limit 실패 분류 (413)"
|
|
- code: PAYLOAD_TOO_LARGE
|
|
category: VALIDATION
|
|
http_status: 413
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Request payload is too large"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# source: feature-api-contract-baseline.md D8 형제 — "URI 길이 실패 분류 (414)"
|
|
# NOTE: enforcement is Tomcat/gateway-owned (rejected before Spring dispatch);
|
|
# this row + code exist for status-mapping consistency. End-to-end 414 contract
|
|
# test is `planned` (gateway/Tomcat maxHttpHeaderSize 8KB boundary).
|
|
- code: URI_TOO_LONG
|
|
category: VALIDATION
|
|
http_status: 414
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Request URI is too long"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# source: feature-api-contract-baseline.md D9 — "415 Unsupported Media Type = 요청 본문 format 미지원"
|
|
- code: UNSUPPORTED_MEDIA_TYPE
|
|
category: VALIDATION
|
|
http_status: 415
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-api-contract-baseline
|
|
owner_layer: presentation
|
|
client_safe_message: "Request Content-Type is not supported"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:api-contract-status-mapping
|
|
|
|
# ============================================================
|
|
# Tenant (feature-tenant-context-policy)
|
|
# ============================================================
|
|
|
|
# source: feature-tenant-context-policy L71 — "tenant 미지원 모드에서 X-Tenant-Id 헤더 수신 시 400 TENANT_NOT_SUPPORTED (filter 단계)"
|
|
- code: TENANT_NOT_SUPPORTED
|
|
category: VALIDATION
|
|
http_status: 400
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-tenant-context-policy
|
|
owner_layer: presentation
|
|
client_safe_message: "Tenant context is not supported by this deployment"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:tenant-policy
|
|
|
|
# ============================================================
|
|
# Validation / Business rule (feature-business-rule-validation-contract)
|
|
# ============================================================
|
|
# NOTE: business-rule-validation branch는 mapping 규칙 SSOT (syntax→VALIDATION,
|
|
# policy→AUTHZ/CONFLICT, invariant→CONFLICT/VALIDATION, persistence→PERSISTENCE/CONFLICT)
|
|
# 이며 구체 code는 example로 VALIDATION_EMAIL_FORMAT만 등장
|
|
# (feature-operational-error-observability-foundation L110). 실제 도메인별 code는
|
|
# Phase D(도메인 feature 적용) 시 본 registry에 추가.
|
|
|
|
# source: feature-operational-error-observability-foundation L110 — "code: VALIDATION_EMAIL_FORMAT, // registry-registered code" (validation field error JSON shape example)
|
|
- code: VALIDATION_EMAIL_FORMAT
|
|
category: VALIDATION
|
|
http_status: 400
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-operational-error-observability-foundation
|
|
owner_layer: presentation
|
|
client_safe_message: "Invalid email format"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: none
|
|
required_test: contract-verification:validation-envelope
|
|
|
|
# ============================================================
|
|
# Cache (feature-cache-consistency-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-cache-consistency-contract — Decisionized Work Items "Redis unavailable | degrade only if declared | fail-fast for required cache | generic INTERNAL | unavailable mapping" / 테스트 "Redis unavailable이 degrade 가능 여부 없이 INTERNAL로 처리되면 실패"
|
|
- code: CACHE_UNAVAILABLE
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 2
|
|
owner_branch: feature-cache-consistency-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Service temporarily unavailable"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://cache/unavailable"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:cache-consistency
|
|
|
|
# source: feature-cache-consistency-contract L70 — "stampede 방지 default = single-instance Caffeine local lock, multi-instance HPA 시 Redisson RLock distributed mutex" / 테스트 "동일 key에 대해 동시 cache miss 시 backend 호출이 1회로 제한되는지 verify (stampede). 미충족 시 실패"
|
|
- code: CACHE_STAMPEDE_LOCK_TIMEOUT
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 1
|
|
owner_branch: feature-cache-consistency-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Service temporarily unavailable"
|
|
log_level: WARN
|
|
runbook_link: "runbook://cache/stampede-lock-timeout"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:cache-consistency
|
|
|
|
# ============================================================
|
|
# Outbound HTTP (feature-outbound-http-client-baseline)
|
|
# ============================================================
|
|
|
|
# source: feature-outbound-http-client-baseline L70 — "outbound HTTP timeout default = connect 2s / read 5s / global call 10s" + scope "timeout/connect/DNS failure 분류" / 테스트 "upstream timeout은 retryable dependency failure로 분류되어야 함"
|
|
- code: DEPENDENCY_TIMEOUT
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 504
|
|
retryable: true
|
|
retry_after_seconds: 2
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service did not respond in time, please retry"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://dependency/timeout"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# source: feature-outbound-http-client-baseline — scope "timeout/connect/DNS failure 분류" + L70 connect=2s timeout
|
|
- code: DEPENDENCY_CONNECT_FAILED
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 2
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service unreachable, please retry"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://dependency/connect-failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# source: feature-outbound-http-client-baseline — scope "timeout/connect/DNS failure 분류"
|
|
- code: DEPENDENCY_DNS_FAILED
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service unreachable, please retry"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://dependency/dns-failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# source: feature-outbound-http-client-baseline — scope "upstream 4xx/5xx 분류" / 테스트 "401/403은 credential/scope/config 문제로 분류되어야 함"
|
|
- code: DEPENDENCY_4XX_CLIENT
|
|
category: PERMANENT_DEPENDENCY
|
|
http_status: 502
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service rejected the request"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://dependency/4xx-client"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# source: feature-outbound-http-client-baseline — scope "upstream 4xx/5xx 분류"
|
|
- code: DEPENDENCY_5XX_SERVER
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 502
|
|
retryable: true
|
|
retry_after_seconds: 2
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service error, please retry"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://dependency/5xx-server"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# source: feature-outbound-http-client-baseline L69 — "circuit breaker metric은 dependency.name, dependency.type, outcome까지만 tag로 허용" + Decisionized "circuit breaker | Resilience4j optional env"
|
|
- code: DEPENDENCY_CIRCUIT_OPEN
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 10
|
|
owner_branch: feature-outbound-http-client-baseline
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Upstream service temporarily unavailable, please retry later"
|
|
log_level: WARN
|
|
runbook_link: "runbook://dependency/circuit-open"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbound-http
|
|
|
|
# ============================================================
|
|
# Outbox (feature-domain-event-outbox-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-domain-event-outbox-contract L67 — "outbox row status enum = PENDING / IN_FLIGHT / PUBLISHED / FAILED / DEAD" + scope "publish 실패 분류" / 판정 "publish 실패가 retry/DLQ/log/runbook 기준 없이 삼켜지면 실패"
|
|
- code: OUTBOX_PUBLISH_FAILED
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 500
|
|
retryable: true
|
|
retry_after_seconds: 30
|
|
owner_branch: feature-domain-event-outbox-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://outbox/publish-failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbox-publish
|
|
|
|
# source: feature-domain-event-outbox-contract L67 — outbox status enum "DEAD" / Outbox Defaults "DLQ | background-job branch owner"
|
|
- code: OUTBOX_DEAD_LETTER
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-domain-event-outbox-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://outbox/dead-letter"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:outbox-dlq
|
|
|
|
# ============================================================
|
|
# Background job / Async (feature-background-job-async-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-background-job-async-contract — Decisionized "saturation | bounded executor + rejection log" / L72 "saturation policy default = AbortPolicy" / 테스트 "executor rejection이 structured log 없이 발생하면 실패"
|
|
- code: JOB_EXECUTOR_REJECTED
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-background-job-async-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Service temporarily unavailable"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://job/executor-rejected"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:async-saturation
|
|
|
|
# source: feature-background-job-async-contract L69 — "기본 backoff는 exponential backoff with jitter, max attempts 3, DLQ after exhausted attempts" + scope "shutdown 중 job 처리 기준" / L73 graceful shutdown ≤19s
|
|
- code: JOB_TIMEOUT
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 500
|
|
retryable: true
|
|
retry_after_seconds: 10
|
|
owner_branch: feature-background-job-async-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://job/timeout"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:async-timeout
|
|
|
|
# source: feature-background-job-async-contract L69 — "DLQ after exhausted attempts" + Decisionized "retry/DLQ | exp backoff jitter, max 3, DLQ exhausted | ... | infinite retry"
|
|
- code: JOB_DEAD_LETTER
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-background-job-async-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://job/dead-letter"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:async-dlq
|
|
|
|
# ============================================================
|
|
# Distributed Lock (feature-distributed-lock-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-distributed-lock-contract D7 — "lock 획득 실패/timeout 의 error code =
|
|
# LOCK_ACQUISITION_TIMEOUT (category CONFLICT, retryable true, client_safe true) + metric
|
|
# lock.acquisition" / D5 — "try-lock + 유한 waitTime + lease(TTL) 필수, 무한 blocking 금지".
|
|
# category CONFLICT 는 기존 enum 재사용; retryable=true — 락 보유자가 임계 구역을 빠져나오면
|
|
# 동일 요청 재시도로 해소된다(transient contention). DB_DEADLOCK / DB_SERIALIZATION_FAILURE 와
|
|
# 같은 retryable CONFLICT 계열(409). 본 코드는 distributedLockProvider 획득 timeout 전용이며
|
|
# cache stampede lock 의 CACHE_STAMPEDE_LOCK_TIMEOUT(cache-consistency, TRANSIENT_DEPENDENCY 503)
|
|
# 과 의미가 구분된다 — 후자는 캐시 백엔드 의존성 timeout, 전자는 분산 상호배제 contention.
|
|
- code: LOCK_ACQUISITION_TIMEOUT
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: true
|
|
retry_after_seconds: 1
|
|
owner_branch: feature-distributed-lock-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Resource is busy, please retry"
|
|
log_level: WARN
|
|
runbook_link: "runbook://lock/acquisition-timeout"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:lock-acquisition-timeout
|
|
|
|
# ============================================================
|
|
# Migration / Startup (feature-migration-startup-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-migration-startup-contract L71 — "startup exit code 표준 = ... migration 실패=70 ..." + Decisionized "startup failure log | structured log with startup.phase, error.code, error.category"
|
|
- code: MIGRATION_FAILED
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-migration-startup-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://migration/failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:migration-startup
|
|
|
|
# source: feature-migration-startup-contract L71 — "startup exit code 표준 = env 누락/malformed=78 ..." / 테스트 "required env 누락 시 startup이 성공하면 실패"
|
|
- code: STARTUP_VALIDATION_FAILED
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-migration-startup-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://startup/validation-failed"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:migration-startup
|
|
|
|
# source: feature-migration-startup-contract L71 — "startup exit code 표준 = ... required adapter disabled=72" / 테스트 "disabled required adapter로 app이 뜨면 실패"
|
|
- code: REQUIRED_ADAPTER_DISABLED
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-migration-startup-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://startup/required-adapter-disabled"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:migration-startup
|
|
|
|
# source: feature-integration-adapter-templates §구현 가이드 §4 (Layer 3) + §Audit A2.
|
|
# Runtime-lifecycle fail-fast for an invoke against a DISABLED optional adapter
|
|
# (Kafka/Redis/Slack/Google Email). Deliberately distinct from the startup-lifecycle
|
|
# REQUIRED_ADAPTER_DISABLED above (exit 72): a runtime invoke ≠ a startup validation,
|
|
# so reusing the startup code would conflate two lifecycles (A2 resolution — new
|
|
# runtime code owned by this branch). retryable=false: the adapter stays disabled
|
|
# until redeploy, so retrying the same call never clears it.
|
|
- code: ADAPTER_DISABLED
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-integration-adapter-templates
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://adapter/adapter-disabled"
|
|
compatibility_impact: none
|
|
required_test: adapter-contract:adapter-disabled-runtime-call
|
|
|
|
# source: feature-migration-startup-contract L71 — "startup exit code 표준 = ... profile mismatch=71" / 테스트 "prod profile에서 local-only 설정이 켜지면 실패"
|
|
- code: PROFILE_MISMATCH
|
|
category: INTERNAL
|
|
http_status: 500
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-migration-startup-contract
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Internal server error"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://startup/profile-mismatch"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:migration-startup
|
|
|
|
# ============================================================
|
|
# Management / Actuator (feature-management-actuator-security-contract)
|
|
# ============================================================
|
|
|
|
# source: feature-management-actuator-security-contract — Exposure Policy "env/configprops | forbidden" "heapdump/threaddump | forbidden unless break-glass runbook" "shutdown | forbidden" / 테스트 "prod에서 env/configprops endpoint가 노출되면 실패"
|
|
- code: ACTUATOR_FORBIDDEN
|
|
category: AUTHZ
|
|
http_status: 403
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-management-actuator-security-contract
|
|
owner_layer: presentation
|
|
client_safe_message: "Permission denied"
|
|
log_level: WARN
|
|
runbook_link: "runbook://management/actuator-forbidden"
|
|
compatibility_impact: none
|
|
required_test: contract-verification:management-actuator
|
|
|
|
# ============================================================
|
|
# TECH LOG STUDIO (studio-v1.yaml ApiError.code — 23종, feature-techlog-studio-backend)
|
|
#
|
|
# StudioError(dev.caskeleton.application.techlog.error.StudioError)와 1:1 매핑.
|
|
# category/http_status/retryable은 그 enum의 선언과 정확히 같아야 한다
|
|
# (StudioErrorRegistryTest가 코드 존재만 보고, 값 일치는 이 파일의 리뷰 책임).
|
|
#
|
|
# PAYLOAD_TOO_LARGE / UNSUPPORTED_MEDIA_TYPE은 StudioError에도 있지만 별도 row를
|
|
# 추가하지 않는다 — feature-api-contract-baseline이 이미 동일 code로 아래(L514,
|
|
# L545)에 VALIDATION/413/415/false, VALIDATION/415/false row를 갖고 있고 값이
|
|
# StudioError 선언과 정확히 일치한다. error-codes.yaml의 identity column은 `code`
|
|
# 하나뿐이라 같은 code로 두 번째 row를 추가하면 ContractRegistrySchemaGovernanceTest
|
|
# 의 "duplicate identity" 게이트가 깨진다. 즉 이 두 코드는 기존 row가 이미 커버한다.
|
|
# ============================================================
|
|
|
|
# source: studio-v1.yaml ApiError.code — AUTHENTICATION_REQUIRED (StudioError.AUTHENTICATION_REQUIRED)
|
|
- code: AUTHENTICATION_REQUIRED
|
|
category: AUTH
|
|
http_status: 401
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: presentation
|
|
client_safe_message: "Studio 인증이 필요합니다"
|
|
log_level: INFO
|
|
runbook_link: "runbook://auth/token-missing"
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — STUDIO_ACCESS_DENIED (StudioError.STUDIO_ACCESS_DENIED)
|
|
- code: STUDIO_ACCESS_DENIED
|
|
category: AUTHZ
|
|
http_status: 403
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "이 Studio 리소스에 접근할 권한이 없습니다"
|
|
log_level: WARN
|
|
runbook_link: "runbook://authz/insufficient-permission"
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — DOCUMENT_NOT_FOUND (StudioError.DOCUMENT_NOT_FOUND)
|
|
- code: DOCUMENT_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 문서를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — VERSION_CONFLICT (StudioError.VERSION_CONFLICT)
|
|
- code: VERSION_CONFLICT
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "저장된 version이 더 최신입니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — REQUEST_VALIDATION_FAILED (StudioError.REQUEST_VALIDATION_FAILED)
|
|
- code: REQUEST_VALIDATION_FAILED
|
|
category: VALIDATION
|
|
http_status: 422
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: presentation
|
|
client_safe_message: "요청 형식이 올바르지 않습니다"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — DOCUMENT_VALIDATION_FAILED
|
|
# (StudioError.DOCUMENT_VALIDATION_FAILED). 원래 계약 이름은 VALIDATION_FAILED였으나
|
|
# 스켈레톤 전역 OperationalError.VALIDATION_FAILED(400, VALIDATION)와 code 문자열이
|
|
# 충돌해(같은 문자열, 다른 http_status) 개명했다 — controller 판정, 2026-08-19.
|
|
- code: DOCUMENT_VALIDATION_FAILED
|
|
category: VALIDATION
|
|
http_status: 422
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "문서 검증에 실패했습니다"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — VALIDATION_STALE (StudioError.VALIDATION_STALE)
|
|
- code: VALIDATION_STALE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "검증 결과가 최신 문서 기준이 아닙니다. 다시 검증해 주세요"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PREVIEW_NOT_FOUND (StudioError.PREVIEW_NOT_FOUND)
|
|
- code: PREVIEW_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 미리보기를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PREVIEW_STALE (StudioError.PREVIEW_STALE)
|
|
- code: PREVIEW_STALE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "미리보기가 최신 문서 기준이 아닙니다. 다시 생성해 주세요"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PREVIEW_EXPIRED (StudioError.PREVIEW_EXPIRED)
|
|
- code: PREVIEW_EXPIRED
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "미리보기가 만료되었습니다. 다시 생성해 주세요"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PUBLICATION_NOT_FOUND (StudioError.PUBLICATION_NOT_FOUND)
|
|
- code: PUBLICATION_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 게시물을 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PUBLICATION_CONFLICT (StudioError.PUBLICATION_CONFLICT)
|
|
- code: PUBLICATION_CONFLICT
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "게시 작업이 다른 변경과 충돌했습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PUBLICATION_EVENT_NOT_FOUND (StudioError.PUBLICATION_EVENT_NOT_FOUND)
|
|
- code: PUBLICATION_EVENT_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 게시 이벤트를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — PUBLICATION_SNAPSHOT_NOT_FOUND (StudioError.PUBLICATION_SNAPSHOT_NOT_FOUND)
|
|
- code: PUBLICATION_SNAPSHOT_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 게시 스냅샷을 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — WARNING_ACKNOWLEDGEMENT_REQUIRED (StudioError.WARNING_ACKNOWLEDGEMENT_REQUIRED)
|
|
- code: WARNING_ACKNOWLEDGEMENT_REQUIRED
|
|
category: VALIDATION
|
|
http_status: 422
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "경고 확인이 필요합니다. 확인 후 다시 시도해 주세요"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — IDEMPOTENCY_KEY_REUSED (StudioError.IDEMPOTENCY_KEY_REUSED)
|
|
- code: IDEMPOTENCY_KEY_REUSED
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "Idempotency 키가 다른 요청에 재사용되었습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — ASSET_NOT_FOUND (StudioError.ASSET_NOT_FOUND)
|
|
- code: ASSET_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "요청한 자산을 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — ASSET_NOT_READY (StudioError.ASSET_NOT_READY)
|
|
- code: ASSET_NOT_READY
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "자산 처리가 아직 완료되지 않았습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — ASSET_IN_USE (StudioError.ASSET_IN_USE)
|
|
- code: ASSET_IN_USE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "자산이 사용 중이라 이 작업을 수행할 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — ASSET_QUARANTINED (StudioError.ASSET_QUARANTINED)
|
|
- code: ASSET_QUARANTINED
|
|
category: DATA_INTEGRITY
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: application
|
|
client_safe_message: "자산이 격리 처리되어 사용할 수 없습니다"
|
|
log_level: WARN
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# source: studio-v1.yaml ApiError.code — STUDIO_UNAVAILABLE (StudioError.STUDIO_UNAVAILABLE)
|
|
- code: STUDIO_UNAVAILABLE
|
|
category: TRANSIENT_DEPENDENCY
|
|
http_status: 503
|
|
retryable: true
|
|
retry_after_seconds: 5
|
|
owner_branch: feature-techlog-studio-backend
|
|
owner_layer: infrastructure
|
|
client_safe_message: "Studio 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해 주세요"
|
|
log_level: ERROR
|
|
runbook_link: "runbook://studio/unavailable"
|
|
compatibility_impact: additive
|
|
required_test: StudioErrorTest
|
|
|
|
# === Tech Log Public (feature-techlog-public-v1) ===
|
|
#
|
|
# public-v1.yaml 의 ApiError.code 는 세 값이다. 나머지 하나 INTERNAL_ERROR 는 스켈레톤
|
|
# 공통 코드로 이미 이 레지스트리에 있으므로 여기서 다시 선언하지 않는다.
|
|
#
|
|
# Studio 와 이름을 겹치지 않게 한 이유: 이 레지스트리는 코드 하나에 http_status 하나만
|
|
# 담는다. public 의 400 과 studio 의 422 를 같은 이름으로 쓸 수 없다.
|
|
|
|
# source: public-v1.yaml ApiError.code — PUBLIC_REQUEST_INVALID (PublicError.PUBLIC_REQUEST_INVALID)
|
|
- code: PUBLIC_REQUEST_INVALID
|
|
category: VALIDATION
|
|
http_status: 400
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-public-v1
|
|
owner_layer: application
|
|
client_safe_message: "요청 값이 올바르지 않습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: PublicErrorRegistryTest
|
|
|
|
# source: public-v1.yaml ApiError.code — PUBLIC_RESOURCE_NOT_FOUND (PublicError.PUBLIC_RESOURCE_NOT_FOUND)
|
|
- code: PUBLIC_RESOURCE_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-public-v1
|
|
owner_layer: application
|
|
client_safe_message: "요청한 자료를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: PublicErrorRegistryTest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TECH LOG STUDIO MANAGEMENT (studio-management-v1.yaml ApiError.code)
|
|
#
|
|
# ManagementError(dev.caskeleton.application.techlog.error.ManagementError)와 1:1.
|
|
# AUTHENTICATION_REQUIRED / STUDIO_ACCESS_DENIED / REQUEST_VALIDATION_FAILED /
|
|
# VERSION_CONFLICT 는 StudioError 에도 있어 행이 이미 존재한다 — 이 파일의 identity
|
|
# column 은 `code` 이므로 중복 행을 만들지 않는다.
|
|
# ---------------------------------------------------------------------------
|
|
# source: studio-management-v1.yaml ApiError.code — TOPIC_NOT_FOUND (ManagementError.TOPIC_NOT_FOUND)
|
|
- code: TOPIC_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "주제를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — TOPIC_NAME_TAKEN (ManagementError.TOPIC_NAME_TAKEN)
|
|
- code: TOPIC_NAME_TAKEN
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "같은 이름의 주제가 이미 있습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — TOPIC_SLUG_TAKEN (ManagementError.TOPIC_SLUG_TAKEN)
|
|
- code: TOPIC_SLUG_TAKEN
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "같은 slug 의 주제가 이미 있습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — TOPIC_IN_USE (ManagementError.TOPIC_IN_USE)
|
|
- code: TOPIC_IN_USE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "이 주제를 쓰는 기록이 있어 삭제할 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — PROJECT_NOT_FOUND (ManagementError.PROJECT_NOT_FOUND)
|
|
- code: PROJECT_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "프로젝트를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — PROJECT_SLUG_TAKEN (ManagementError.PROJECT_SLUG_TAKEN)
|
|
- code: PROJECT_SLUG_TAKEN
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "같은 slug 의 프로젝트가 이미 있습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — PROJECT_IN_USE (ManagementError.PROJECT_IN_USE)
|
|
- code: PROJECT_IN_USE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "이 프로젝트에 연결된 기록이 있어 삭제할 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — RELEASE_NOT_FOUND (ManagementError.RELEASE_NOT_FOUND)
|
|
- code: RELEASE_NOT_FOUND
|
|
category: NOT_FOUND
|
|
http_status: 404
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "요청한 릴리즈를 찾을 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — RELEASE_VERSION_TAKEN (ManagementError.RELEASE_VERSION_TAKEN)
|
|
- code: RELEASE_VERSION_TAKEN
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "같은 버전의 릴리즈가 이미 있습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest
|
|
# source: studio-management-v1.yaml ApiError.code — RELEASE_NOT_PUBLISHABLE (ManagementError.RELEASE_NOT_PUBLISHABLE)
|
|
- code: RELEASE_NOT_PUBLISHABLE
|
|
category: CONFLICT
|
|
http_status: 409
|
|
retryable: false
|
|
retry_after_seconds: null
|
|
owner_branch: feature-techlog-management-v1
|
|
owner_layer: application
|
|
client_safe_message: "지금 상태에서는 발행할 수 없습니다"
|
|
log_level: INFO
|
|
runbook_link: null
|
|
compatibility_impact: additive
|
|
required_test: ManagementErrorRegistryTest |