ZUKU API — 오류 · 응답 봉투

> 브랜드: ZUKU API

> 구현 기준: backend/rs/src/router.rs (envelope_ok / envelope_err / format_response), upload.rs

> 클라이언트의 ApiFailure(code, message, status) 패턴과 맞춘다.


응답 봉투 (Envelope)

모든 JSON API 응답은 공통 봉투를 사용한다 (models.rs · ApiMeta / ApiError).

성공

{
  "success": true,
  "data": { },
  "meta": {
    "request_id": "…",
    "timestamp": "2026-08-22T00:00:00Z",
    "version": "v1"
  }
}
  • data: 엔드포인트별 페이로드
  • meta.version: 현재 고정 "v1" (URL /api/v1 · 헤더 X-API-Version과 동일 세대)

실패

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "입력값을 확인해 주세요",
    "details": [
      { "field": "title", "message": "제목은 1~100자여야 합니다" }
    ]
  },
  "meta": {
    "request_id": "…",
    "timestamp": "…",
    "version": "v1"
  }
}
  • details필드 단위 검증이 있을 때만 포함 (envelope_err_details)
  • 단순 실패는 code + message만 (envelope_err)

예외

상황 본문
204 No Content 빈 본문 (댓글 삭제·일부 폐기 API 등)
Jump stream / swf 성공 JSON이 아닌 바이너리 가능
CORS preflight 204

HTTP 상태 코드 (구현에서 쓰는 범위)

HTTP 대표 상황
200 OK 조회·갱신·토글 성공
201 Created 생성(콘텐츠·업로드·가입·글 등)
204 No Content 본문 없는 성공 / OPTIONS
308 Permanent Redirect 레거시 Jump 경로 → 정규 API 안내
400 Bad Request JSON/multipart 형식 오류 (BAD_REQUEST)
401 Unauthorized 세션/토큰 없음·무효 (UNAUTHORIZED)
403 Forbidden 권한 부족 (FORBIDDEN)
404 Not Found 리소스/경로 없음 (코드는 리소스별)
409 Conflict 중복·상태 충돌 (EMAIL_EXISTS, HANDLE_EXISTS, ALREADY_AUTHENTICATED, LEGACY_ACCOUNT_EXISTS, SELF_ACTION_FORBIDDEN 등)
413 Payload Too Large 업로드 한도 (PAYLOAD_TOO_LARGE)
415 Unsupported Media Type 매직바이트 미지원 (UNSUPPORTED_MEDIA_TYPE)
422 Unprocessable Entity 필드 검증 (VALIDATION_ERROR, Jump ID 검증 코드 등)
500 Internal Server Error 서버/DB (INTERNAL_ERROR, DB_UNAVAILABLE)
501 Not Implemented 스코프 아웃 (NOT_IMPLEMENTED, 예: OAuth)
503 Service Unavailable 캡차 미설정 등 (CAPTCHA_NOT_CONFIGURED)

에러 코드 목록

router.rs / upload.rs에서 envelope_err · envelope_err_details · json_not_found실제로 나가는 코드다. (모듈 위임 moderation/analytics의 동일 코드 포함 가능)

인증 · 권한

code 전형적 HTTP 메시지 요지
UNAUTHORIZED 401 Bearer 세션/Authorization 필요, 유효한 access_token 필요
FORBIDDEN 403 권한 없음 · 관리자 전용
ALREADY_AUTHENTICATED 409 이미 로그인된 상태로 회원가입 시도
SELF_ACTION_FORBIDDEN 409 자기 자신에 대한 금지된 관리 조작

검증 · 요청 형식

code 전형적 HTTP 메시지 요지
BAD_REQUEST 400 요청 본문/ multipart 형식 오류
VALIDATION_ERROR 422 필드 검증 실패 (details[] 동반)
INVALID_CONTENT_ID 422 Jump play ID 문자/길이 규칙 위반
CONTENT_ID_MISMATCH 422 Jump 경로 ID ≠ 본문 content_id

회원가입 · 계정

code 전형적 HTTP 메시지 요지
EMAIL_EXISTS 409 이메일 중복
HANDLE_EXISTS 409 handle 중복
LEGACY_ACCOUNT_EXISTS 409 주전자닷컴 레거시 계정과 충돌 — 기존 로그인 유도

캡차

code 전형적 HTTP 메시지 요지
CAPTCHA_FAILED (검증 실패) PoW/캡차 실패
CAPTCHA_NOT_CONFIGURED 503 CAPTCHA_HMAC_SECRET 미설정 — fail-closed

리소스 Not Found (세분화)

code 전형적 HTTP 대상
NOT_FOUND 404 일반(세션·캡차 경로·업로드 파일 등)
CONTENT_NOT_FOUND 404 콘텐츠
COMMENT_NOT_FOUND 404 댓글
POST_NOT_FOUND 404 커뮤니티 글
CONVERSATION_NOT_FOUND 404 DM 대화
USER_NOT_FOUND 404 회원
NOTIFICATION_NOT_FOUND 404 알림
API_KEY_NOT_FOUND 404 개발자 API 키
ROUTE_NOT_FOUND 404 매칭되는 API 경로 없음

미디어 · Jump

code 전형적 HTTP 메시지 요지
UNSUPPORTED_MEDIA_TYPE 415 지원하지 않는 업로드 형식
PAYLOAD_TOO_LARGE 413 업로드 크기 한도 초과
SOURCE_UNAVAILABLE (재생 실패) 재생 가능한 원본 없음
SWF_PARSE_FAILED (재생 실패) SWF 해석/IR 인코딩/비지원 형식

인프라 · 기타

code 전형적 HTTP 메시지 요지
DB_UNAVAILABLE 500 DB 풀/접속 불가
INTERNAL_ERROR 500 내부 처리 실패
NOT_IMPLEMENTED 501 미구현(예: OAuth provider)

> 프론트 ApiFailure는 봉투 파싱 실패 시 INVALID_RESPONSE, 빈 본문 오류 시 HTTP_ERROR클라이언트 측에서 만들 수 있다. 이는 서버 envelope_err 코드가 아니다.


details[] 필드 오류

VALIDATION_ERROR 등에서:

"details": [
  { "field": "title", "message": "제목은 1~100자여야 합니다" },
  { "field": "tags", "message": "태그는 최대 10개입니다" }
]

콘텐츠 생성 시 흔한 field 값: title, description, tags, age_rating, type.

UI는 details를 필드별 인라인 에러로 매핑하고, 없으면 error.message를 토스트/배너로 쓴다.


응답 헤더

format_response가 모든 응답에 붙인다.

헤더 현재 동작
X-API-Version 고정 v1
X-RateLimit-Limit 더미 정적값 1000
X-RateLimit-Remaining 더미 정적값 999
X-RateLimit-Reset 대략 now + 60초(Unix) — 실제 쿼터 차감 없음

> 현재 백엔드는 Rate Limit을 강제하지 않는다. 헤더는 계약 자리표시자(dummy)다. 클라이언트가 Remaining으로 UX를 잠그면 안 된다.

그 외: Access-Control-Allow-Origin: *, 허용 메서드/헤더(Authorization, Content-Type) 등.


처리 팁 (JS / ApiFailure 스타일)

class ApiFailure extends Error {
  constructor(
    public code: string,
    message: string,
    public status: number,
    public details?: { field: string; message: string }[],
  ) {
    super(message);
    this.name = 'ApiFailure';
  }
}

async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`/api/v1${path}`, {
    ...init,
    headers: {
      'Content-Type': 'application/json',
      ...(init?.headers ?? {}),
    },
  });

  if (res.status === 204) return undefined as T;

  const envelope = await res.json();
  if (!envelope.success) {
    throw new ApiFailure(
      envelope.error.code,
      envelope.error.message,
      res.status,
      envelope.error.details,
    );
  }
  return envelope.data as T;
}

// 사용 예
try {
  await apiFetch('/contents', { method: 'POST', body: JSON.stringify(payload), headers: {
    Authorization: `Bearer ${token}`,
  }});
} catch (e) {
  if (e instanceof ApiFailure) {
    switch (e.code) {
      case 'UNAUTHORIZED':
        // 로그인 유도
        break;
      case 'VALIDATION_ERROR':
        // e.details 로 폼 하이라이트
        break;
      case 'EMAIL_EXISTS':
      case 'HANDLE_EXISTS':
        // 가입 폼 전용 메시지
        break;
      case 'CAPTCHA_FAILED':
        // 캡차 재시도
        break;
      case 'DB_UNAVAILABLE':
      case 'INTERNAL_ERROR':
        // 잠시 후 재시도
        break;
      default:
        console.error(e.code, e.message, e.status);
    }
  }
}

관련 페이지