Zuku

에러

Errors

개요

  • 브랜드: 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 preflight204

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

HTTP대표 상황
200 OK조회·갱신·토글 성공
201 Created생성(콘텐츠·업로드·가입·글 등)
204 No Content본문 없는 성공 / OPTIONS
308 Permanent Redirect레거시 Jump 경로 → 정규 API 안내
400 Bad RequestJSON/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메시지 요지
UNAUTHORIZED401Bearer 세션/Authorization 필요, 유효한 access_token 필요
FORBIDDEN403권한 없음 · 관리자 전용
ALREADY_AUTHENTICATED409이미 로그인된 상태로 회원가입 시도
SELF_ACTION_FORBIDDEN409자기 자신에 대한 금지된 관리 조작

검증 · 요청 형식

code전형적 HTTP메시지 요지
BAD_REQUEST400요청 본문/ multipart 형식 오류
VALIDATION_ERROR422필드 검증 실패 (details[] 동반)
INVALID_CONTENT_ID422Jump play ID 문자/길이 규칙 위반
CONTENT_ID_MISMATCH422Jump 경로 ID ≠ 본문 content_id

회원가입 · 계정

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

캡차

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

리소스 Not Found (세분화)

code전형적 HTTP대상
NOT_FOUND404일반(세션·캡차 경로·업로드 파일 등)
CONTENT_NOT_FOUND404콘텐츠
COMMENT_NOT_FOUND404댓글
POST_NOT_FOUND404커뮤니티 글
CONVERSATION_NOT_FOUND404DM 대화
USER_NOT_FOUND404회원
NOTIFICATION_NOT_FOUND404알림
API_KEY_NOT_FOUND404개발자 API 키
ROUTE_NOT_FOUND404매칭되는 API 경로 없음

미디어 · Jump

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

인프라 · 기타

code전형적 HTTP메시지 요지
DB_UNAVAILABLE500DB 풀/접속 불가
INTERNAL_ERROR500내부 처리 실패
NOT_IMPLEMENTED501미구현(예: 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);
    }
  }
}

관련 페이지

On this page