> ## Documentation Index
> Fetch the complete documentation index at: https://dripart-chore-sync-comfy-api-v2-spec.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Comfy Router errors and retries

> Read Comfy Router error responses and validation details, retry with the same Idempotency-Key, and recover after timeouts.

A failed Router call carries its HTTP status, an error bucket in `X-Comfy-Error-Type`, and a request ID in `X-Comfy-Request-Id`. Keep all three, and keep the `Idempotency-Key` you sent, before deciding whether to retry.

## Read errors defensively

A failed request can return a proxy's HTML error page, truncated JSON, or plain text. Do not let a JSON parsing error hide the HTTP status or request ID. These helpers use an `httpx.Response` in Python and a Fetch `Response` in TypeScript; the SDKs already expose error fields for normal SDK calls.

<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}
  // Add to your Package.swift:
  //   .package(url: "https://github.com/Comfy-Org/comfy-swift-sdk.git", from: "0.5.0")
  import ComfySwiftSDK

  // The SDK reads the response defensively for you: a Router failure is thrown as
  // ComfyError.router with the HTTP status, request ID, error bucket and any
  // per-field validation detail already extracted, even when the body was an HTML
  // error page or truncated JSON. Catch it from any client.models call.
  struct RouterErrorInfo {
      let status: Int
      let requestId: String?
      let errorType: String
      let message: String
      let validation: [RouterValidationErrorDetail]
  }

  // `client.models.run` is `async throws`, so an untyped `catch` binds `Error`.
  // Take `Error` here so the caught value can be passed straight in.
  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>

## Validation errors

A Router `422` means validation failed before the provider call and is not billed. Its body has a `detail[]` array, with one entry per rejected field. The error category is in `X-Comfy-Error-Type`, not in the body. For example:

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

This is an example shape. Models with a permissive input schema may forward a missing field to the provider instead of returning a Router `422`.

| Field  | Meaning                                                                           |
| ------ | --------------------------------------------------------------------------------- |
| `loc`  | Path to the rejected field, outermost segment first.                              |
| `msg`  | Human-readable reason for the failure.                                            |
| `type` | Provider-specific reason, such as `missing`, `greater_than` or `image_too_small`. |
| `ctx`  | Optional bound or extra data for that provider error.                             |

`400` describes a request-level problem, such as a malformed cursor, rather than this per-field validation body. The [error reference](/development/comfy-router/reference#error-buckets) lists the supported categories. Treat an unknown category as `internal_error` for control flow, but keep the original value for diagnostics. Do not hard-reject a new error value or implement forecast error categories as though they already occur.

## Retry safely

Persist the key with the model ID and request body **before sending**. Reuse it for every attempt of that logical call. Router does not return the `Idempotency-Key` to you in its response. The Python SDK includes its key on raised exceptions; in TypeScript, keep your supplied key yourself.

Keys are shared within the workspace carried by the credential, or scoped to the user when it carries no workspace. Use a UUID unique across that scope and retry with the same credential. Reusing another workspace member's key can return their recorded result or a conflict; changing credentials can start a separate, billable call.

Router retains keyed response or collection state for 24 hours; a retry does not start a new retention window. Once that state expires, do not expect the old key to recover a result or prevent a new dispatch. A key also does not make an expired asset URL usable again.

## Retry outcomes

| Status                                                         | Bucket                                  | What it means                                                                          | What to do                                                                                                                               |
| -------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `200`                                                          | `Idempotent-Replayed: true` header      | Router replayed a result or returned a collected generation.                           | Use the result; the replay is not a second Comfy charge.                                                                                 |
| `409`                                                          | `concurrency_limit_exceeded`            | The original call for that key is still running.                                       | Wait `Retry-After`, then resend the same key.                                                                                            |
| `504`                                                          | `deadline_exceeded`, with `Retry-After` | Router retained a handle to accepted provider work.                                    | Wait the stated interval and resend the same request and key to collect it. It may still be running.                                     |
| `429`                                                          | `rate_limited`                          | The request allowance is exhausted.                                                    | Wait `Retry-After`, then retry with the same key.                                                                                        |
| `429`                                                          | `concurrency_limit_exceeded`            | The concurrent-call or committed-spend limit refused the request.                      | Reduce concurrency and retry with the same key. Inspect the spend headers.                                                               |
| `409`                                                          | `invalid_input`                         | The request differs from the key's original request, or its record cannot be replayed. | Inspect the conflict. Restore the original request if it changed. Start a new key only when you intend a new, potentially billable call. |
| `504` without a collection hint, another `5xx`, or no response | Varies                                  | The status alone does not identify whether work was accepted, retained, or released.   | Preserve the same key and request. Use a bounded retry policy; recovery is not guaranteed.                                               |

Conflicts compare the method, model path, query, and body. A key can become non-replayable after an oversized response, failed response write, or an asset that cannot be safely replayed. Waiting does not recover a consumed result. A new key starts a new call; it does not retrieve the old output.

A refusal before provider dispatch releases the key. A dispatched call can retain a provider handle or become non-replayable. Do not infer key state or billing from the status code alone.

Do not mint a brand-new key just because a call timed out or the connection dropped. If Router already accepted the generation, a new key can create a second logical run and therefore a second billable outcome. Reuse the same key until you know the original call is unrecoverable.

## Timeouts and collection

One Router call may hold the connection for 10 minutes by default. Set your client timeout above that bound so you keep the typed `504` and the request ID rather than an opaque local abort. If your application cannot hold a connection that long, [queued delivery](/development/comfy-router/queue) returns a `request_id` at once and lets you collect the result later.

`deadline_exceeded` is Router's waiting limit; `provider_timeout` is the provider's deadline. A provider generation that completes can be billed even if the caller received a timeout or disconnected. Client cancellation stops the wait and SDK retries, but does not necessarily cancel accepted provider work.

For submit-and-poll providers, a retained handle lets a same-key request continue collecting the original generation. Dispatched calls cut off without a recoverable handle can consume the key without a replayable result; a same-key retry then returns `409`. Provider-attributed transient failures without a captured success can still release the key for another attempt. The absence of a handle alone does not tell you which outcome applies.

The SDKs retry some failures within a bounded budget. Once they return an error, keep the request and key rather than generating a new one. For raw HTTP, this example retries only the two explicit collection hints:

```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")
```

Pass the original model, body, and saved key. This limits attempts, not total wall time: each call can last up to the client timeout and each wait follows `Retry-After`. HTTP errors retain the response for inspection; transport errors propagate without replacing the key. Schedule later collection with the saved key if your application needs a longer recovery window.

## Next

* [Billing](/development/comfy-router/billing): what a refusal, timeout, or replay costs.
* [Headers](/development/comfy-router/headers): idempotency, request IDs, and retry pacing headers.
* [API reference](/development/comfy-router/reference#error-buckets): every error bucket Router returns.
