> ## Documentation Index
> Fetch the complete documentation index at: https://docs.comfy.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Comfy Router 오류 및 재시도

> Comfy Router 오류 응답과 검증 세부 정보를 확인하고, 동일한 Idempotency-Key로 재시도하며, 타임아웃 이후 복구하는 방법을 설명합니다.

실패한 Router 호출은 HTTP 상태, `X-Comfy-Error-Type`의 오류 버킷, `X-Comfy-Request-Id`의 요청 ID를 함께 전달합니다. 재시도 여부를 결정하기 이전에 이 세 가지와 함께 전송한 `Idempotency-Key`를 보관하세요.

## 오류를 방어적으로 읽기

실패한 요청은 프록시의 HTML 오류 페이지, 잘린 JSON 또는 일반 텍스트를 반환할 수 있습니다. JSON 파싱 오류 때문에 HTTP 상태나 요청 ID가 가려지도록 두지 마세요. 아래 헬퍼는 Python에서는 `httpx.Response`를, TypeScript에서는 Fetch `Response`를 사용합니다. SDK는 일반적인 SDK 호출에 대해 이미 오류 필드를 노출합니다.

<CodeGroup>
  ```python theme={null}
  def read_router_error(response):
      body = None
      if response.headers.get("content-type", "").startswith("application/json"):
          try:
              body = response.json()
          except ValueError:
              body = None

      detail = body.get("detail") if isinstance(body, dict) else None
      return {
          "status": response.status_code,
          "request_id": response.headers.get("X-Comfy-Request-Id"),
          "error_type": response.headers.get("X-Comfy-Error-Type", "internal_error"),
          "message": detail if isinstance(detail, str) else f"HTTP {response.status_code}",
          "validation": detail if isinstance(detail, list) else [],
      }
  ```

  ```typescript theme={null}
  async function readRouterError(response: Response) {
    let body: unknown;
    try {
      body = JSON.parse(await response.text());
    } catch {
      body = undefined;
    }

    const detail =
      typeof body === "object" && body !== null ? (body as { detail?: unknown }).detail : undefined;

    return {
      status: response.status,
      requestId: response.headers.get("X-Comfy-Request-Id"),
      errorType: response.headers.get("X-Comfy-Error-Type") ?? "internal_error",
      message: typeof detail === "string" ? detail : `HTTP ${response.status}`,
      validation: Array.isArray(detail) ? detail : [],
    };
  }
  ```

  ```swift theme={null}
  // Package.swift에 추가:
  //   .package(url: "https://github.com/Comfy-Org/comfy-swift-sdk.git", from: "0.5.0")
  import ComfySwiftSDK

  // SDK가 응답을 방어적으로 읽어 줍니다. Router 실패는 HTTP 상태, 요청 ID, 오류 버킷과
  // 함께 ComfyError.router로 throw되며, 필드별 검증 상세 정보도 이미 추출되어
  // 있습니다. 본문이 HTML 오류 페이지이거나 잘린 JSON이더라도 마찬가지입니다.
  // client.models 호출 어디에서든 이를 잡을 수 있습니다.
  struct RouterErrorInfo {
      let status: Int
      let requestId: String?
      let errorType: String
      let message: String
      let validation: [RouterValidationErrorDetail]
  }

  // `client.models.run`은 `async throws`이므로 타입이 없는 `catch`는 `Error`를 바인딩합니다.
  // 잡힌 값을 그대로 전달할 수 있도록 여기서는 `Error`를 받습니다.
  func readRouterError(_ error: Error) -> RouterErrorInfo? {
      guard let comfyError = error as? ComfyError,
            case .router(let router) = comfyError else { return nil }
      return RouterErrorInfo(
          status: router.httpStatus,
          requestId: router.requestId,
          errorType: router.errorType.rawValue,
          message: router.detail,
          validation: router.validationErrors
      )
  }
  ```
</CodeGroup>

## 검증 오류

Router `422`는 공급자 호출 이전에 유효성 검사가 실패했음을 의미하며, 과금되지 않습니다. 본문에는 `detail[]` 배열이 있고, 거부된 필드마다 항목이 하나씩 있습니다. 오류 카테고리는 본문이 아니라 `X-Comfy-Error-Type`에 있습니다. 예를 들어:

```json theme={null}
{"detail": [{"loc": ["body", "prompt"], "msg": "Field required", "type": "missing"}]}
```

이것은 예시 형태입니다. 관대한 입력 스키마를 가진 모델은 누락된 필드를 Router `422`로 반환하는 대신 공급자에게 전달할 수 있습니다.

| 필드     | 의미                                                              |
| ------ | --------------------------------------------------------------- |
| `loc`  | 거부된 필드의 경로이며, 가장 바깥쪽 세그먼트가 먼저 옵니다.                              |
| `msg`  | 사람이 읽을 수 있는 실패 사유입니다.                                           |
| `type` | 공급자별 사유로, `missing`, `greater_than`, `image_too_small` 등이 있습니다. |
| `ctx`  | 해당 공급자 오류에 대한 선택적 경계값 또는 추가 데이터입니다.                             |

`400`은 이러한 필드별 검증 본문이 아니라 잘못된 커서 같은 요청 수준의 문제를 나타냅니다. [오류 레퍼런스](/ko/development/comfy-router/reference#오류-버킷)는 지원되는 카테고리를 나열합니다. 알 수 없는 카테고리는 제어 흐름상 `internal_error`로 취급하되, 진단을 위해 원본 값은 유지하세요. 새로운 오류 값을 곧바로 거부하지 말고, 예측된 오류 카테고리가 이미 발생하는 것처럼 구현하지 마세요.

## 안전하게 재시도

**전송 이전**에 모델 ID 및 요청 본문과 함께 키를 영구 저장하세요. 해당 논리적 호출의 모든 시도에서 이 키를 재사용하세요. Router는 응답에서 `Idempotency-Key`를 반환하지 않습니다. Python SDK는 발생한 예외에 키를 포함합니다. TypeScript에서는 직접 제공한 키를 보관하세요.

키는 자격 증명이 지닌 워크스페이스 내에서 공유되며, 워크스페이스가 없으면 사용자 범위로 지정됩니다. 해당 범위 전체에서 고유한 UUID를 사용하고 동일한 자격 증명으로 재시도하세요. 다른 워크스페이스 멤버의 키를 재사용하면 그 멤버가 기록한 결과가 반환되거나 충돌이 발생할 수 있습니다. 자격 증명을 변경하면 별도의 과금되는 호출이 시작될 수 있습니다.

Router는 키가 지정된 응답 또는 컬렉션 상태를 24시간 동안 보관합니다. 재시도해도 새로운 보관 기간이 시작되지 않습니다. 해당 상태가 만료되면 이전 키로 결과를 복구하거나 새로운 디스패치를 방지할 수 있다고 기대하지 마세요. 또한 키는 만료된 자산 URL을 다시 사용할 수 있게 해주지 않습니다.

## 재시도 결과

| 상태                                  | 분류                                    | 의미                                          | 대처 방법                                                                 |
| ----------------------------------- | ------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------- |
| `200`                               | `Idempotent-Replayed: true` 헤더        | Router가 결과를 재생했거나 수집된 생성을 반환했습니다.           | 결과를 사용하세요. 재생은 두 번째 Comfy 과금이 아닙니다.                                   |
| `409`                               | `concurrency_limit_exceeded`          | 해당 키의 원본 호출이 아직 실행 중입니다.                    | `Retry-After`만큼 기다린 후 같은 키로 다시 보내세요.                                  |
| `504`                               | `deadline_exceeded`, `Retry-After` 포함 | Router가 수락된 공급자 작업에 대한 핸들을 보유하고 있습니다.       | 명시된 간격만큼 기다린 뒤 동일한 요청과 키를 다시 보내 수집하세요. 아직 실행 중일 수 있습니다.               |
| `429`                               | `rate_limited`                        | 요청 허용량이 소진되었습니다.                            | `Retry-After`만큼 기다린 후 같은 키로 재시도하세요.                                   |
| `429`                               | `concurrency_limit_exceeded`          | 동시 호출 또는 확정 지출 한도가 요청을 거부했습니다.              | 동시성을 줄이고 같은 키로 재시도하세요. 지출 헤더를 확인하세요.                                  |
| `409`                               | `invalid_input`                       | 요청이 해당 키의 원본 요청과 다르거나, 그 기록을 재생할 수 없습니다.    | 충돌을 확인하세요. 원본 요청이 변경되었다면 복원하세요. 새롭게 과금될 수 있는 호출을 의도한 경우에만 새 키를 시작하세요. |
| 수집 힌트가 없는 `504`, 다른 `5xx`, 또는 응답 없음 | 경우에 따라 다름                             | 상태만으로는 작업이 수락되었는지, 보유되었는지, 해제되었는지 알 수 없습니다. | 동일한 키와 요청을 유지하세요. 제한된 재시도 정책을 사용하세요. 복구가 보장되지는 않습니다.                  |

충돌은 방법, 모델 경로, 쿼리, 본문을 비교합니다. 키는 응답 크기 초과, 응답 쓰기 실패, 또는 안전하게 재생할 수 없는 자산 이후에 재생 불가능해질 수 있습니다. 기다린다고 해서 이미 소비된 결과가 복구되지는 않습니다. 새 키는 새 호출을 시작할 뿐이며, 이전 출력을 가져오지 않습니다.

공급자 디스패치 이전의 거부는 키를 해제합니다. 디스패치된 호출은 공급자 핸들을 보유하거나 재생 불가능해질 수 있습니다. 상태 코드만으로 키 상태나 과금을 추론하지 마세요.

호출이 시간 초과되었거나 연결이 끊어졌다는 이유만으로 완전히 새로운 키를 발급하지 마세요. Router가 이미 생성을 수락했다면, 새 키는 두 번째 논리적 실행을 생성하고 따라서 두 번째 과금 결과를 만들 수 있습니다. 원본 호출이 복구 불가능하다는 것을 알기 전까지는 같은 키를 재사용하세요.

## 타임아웃과 결과 수집

Router 호출 하나는 기본적으로 연결을 10분 동안 유지할 수 있습니다. 클라이언트 타임아웃을 그 한도보다 크게 설정하면 불투명한 로컬 중단 대신 타입이 지정된 `504`와 요청 ID를 받을 수 있습니다. 애플리케이션이 그렇게 오래 연결을 유지할 수 없다면, [대기 중 전달](/ko/development/comfy-router/queue)이 즉시 `request_id`를 반환하고 나중에 결과를 수집할 수 있게 해줍니다.

`deadline_exceeded`는 Router의 대기 한도이고, `provider_timeout`은 공급자의 데드라인입니다. 완료된 공급자 생성은 호출자가 타임아웃을 받았거나 연결이 해제되었더라도 과금될 수 있습니다. 클라이언트 취소는 대기와 SDK 재시도를 중단하지만, 수락된 공급자 작업을 반드시 취소하지는 않습니다.

제출 후 폴링하는 공급자의 경우, 보존된 핸들을 사용하면 동일한 키의 요청이 원본 생성을 계속 수집할 수 있습니다. 복구 가능한 핸들 없이 중단된 디스패치 호출은 재생 가능한 결과 없이 키를 소모할 수 있으며, 그러면 동일한 키로 재시도할 때 `409`가 반환됩니다. 성공이 캡처되지 않은 채 공급자에게 귀속된 일시적 실패는 여전히 키를 해제하여 다른 시도를 허용할 수 있습니다. 핸들이 없다는 사실만으로는 어떤 결과가 적용되는지 알 수 없습니다.

SDK는 제한된 예산 내에서 일부 실패를 재시도합니다. 오류가 반환되면 새 요청과 키를 생성하지 말고 요청과 키를 그대로 유지하세요. 순수 HTTP의 경우, 이 예시는 두 가지 명시적 수집 힌트에 대해서만 재시도합니다:

```python theme={null}
import os
import time

import httpx


def collect(model, arguments, key, attempts=3):
    with httpx.Client(timeout=httpx.Timeout(660.0, connect=10.0)) as client:
        for attempt in range(attempts):
            response = client.post(
                f"https://api.comfy.org/v2/models/{model}",
                headers={"X-API-Key": os.environ["COMFY_API_KEY"],
                         "Idempotency-Key": key},
                json=arguments,
            )
            if response.is_success:
                return response.json()

            category = response.headers.get("X-Comfy-Error-Type")
            collecting = (response.status_code, category) in {
                (409, "concurrency_limit_exceeded"),
                (504, "deadline_exceeded"),
            }
            delay = response.headers.get("Retry-After", "")
            if not collecting or not delay.isdigit() or attempt == attempts - 1:
                response.raise_for_status()
            time.sleep(int(delay))
    raise ValueError("attempts must be positive")
```

원본 모델, 본문, 저장된 키를 전달하세요. 이는 총 소요 시간이 아니라 시도 횟수를 제한합니다. 각 호출은 클라이언트 타임아웃까지 지속될 수 있고, 각 대기는 `Retry-After`를 따릅니다. HTTP 오류는 검사를 위해 응답을 유지하고, 전송 오류는 키를 교체하지 않은 채로 전파됩니다. 애플리케이션에 더 긴 복구 창이 필요하면 저장된 키로 나중에 수집을 예약하세요.

## 다음

* [청구](/ko/development/comfy-router/billing): 거부, 타임아웃 또는 재전송(replay)에 드는 비용.
* [헤더](/ko/development/comfy-router/headers): 멱등성, 요청 ID, 재시도 속도 조절 헤더.
* [API 레퍼런스](/ko/development/comfy-router/reference#오류-버킷): Router가 반환하는 모든 오류 버킷.
