SETTO API

Errores

La forma del cuerpo de error, todos los códigos que devuelve la API externa de SETTO, qué significa cada uno y si reintentar sirve de algo.

Los errores regresan como JSON con el estado HTTP que esperarías y un code legible por máquina sobre el que puedes ramificar. Ramifica sobre code, no sobre el message legible por humanos — los mensajes pueden cambiar, los códigos son parte del contrato. Hay una forma para cada error deliberado y una segunda, un poco distinta, para los fallos de validación de la petición.

El cuerpo de error

404 Not Found
{
  "statusCode": 404,
  "code": "TOURNAMENT_NOT_FOUND",
  "message": "TOURNAMENT_NOT_FOUND"
}
CampoTipoNotas
statusCodenumberRefleja el estado HTTP
codestringEstable, en mayúsculas con guiones bajos. Ramifica sobre este
messagestringLegible para humanos; puede ser igual a code

Todos los códigos

EstadocodeSignificado¿Reintentar?
400INVALID_IDUn identificador de ruta no es un UUID v4. Se rechaza antes de ejecutar el handler, así que nunca llega a ser un 404No — corrige la petición
400(validación, ver abajo)Un parámetro de consulta falló la validación — un valor de populate desconocido, un limit fuera de 1..100, un division que no es UUIDNo — corrige la petición
401MISSING_API_TOKENNo hay cabecera Authorization, o no es Bearer setto_…No
401INVALID_API_TOKENToken desconocido, revocado o de otro entornoNo
401EXPIRED_API_TOKENEl expiresAt del token ya pasóNo — crea un token nuevo
403INSUFFICIENT_SCOPEToken válido, pero le falta el alcance que esta ruta necesitaNo
404TOURNAMENT_NOT_FOUNDNo existe ese torneo para esta organizaciónNo
404ROUND_NOT_FOUNDNo existe esa ronda para esta organizaciónNo
429RATE_LIMITEDMás de 120 peticiones en los últimos 60 segundos para este tokenSí — después de Retry-After

Cualquier cosa en el rango 5xx es una falla del lado de SETTO. Reintenta con backoff exponencial y jitter; si persiste, el payload que pedías probablemente no es el problema.

Errores de validación

Un 400 de la capa global de validación es la única respuesta que no usa la forma uniforme: su message es un arreglo de strings, uno por regla fallida.

400 Bad Request
{
  "statusCode": 400,
  "message": [
    "must be a valid enum value,club,club.courts,divisions,divisions.rounds,sponsors,circuit,circuitCategory,rankedTiebreakRules"
  ],
  "error": "Bad Request"
}

Manéjalo a la defensiva:

const body = await res.json();
const message = Array.isArray(body.message)
  ? body.message.join('; ')
  : body.message;

Las causas habituales, por frecuencia:

  1. Un valor de populate que no está en la lista blanca de ese endpoint — las listas están en Populate, y el mensaje de error te enumera los valores legales.
  2. limit fuera de 1..100, u offset menor que 0.
  3. Un valor de status o sport que no es miembro del enum.
  4. Un filtro division que no es un UUID v4.

El otro 400: un id de ruta mal formado

El 400 cubre dos formas distintas. El cuerpo con message como arreglo viene de la capa de parámetros de consulta. Un identificador de ruta que no es un UUID v4 — por ejemplo /v1/external/tournaments/no-es-uuid — se rechaza antes, y responde con la forma uniforme de la API:

400 Bad Request
{
  "statusCode": 400,
  "code": "INVALID_ID",
  "message": "INVALID_ID"
}

La distinción que importa: un idx mal formado es un 400, nunca un 404. Solo un id bien formado que esta organización no puede ver devuelve el 404 de abajo. Es decir, INVALID_ID siempre significa tu cadena está mal, no el recurso no existe.

El 404 esconde más de lo que dice

Un 404 significa esta organización no tiene ese recurso. No distingue entre:

  • un idx que no existe en ningún lado de SETTO;
  • un idx que pertenece a otra organización;
  • un idx que pertenece a un torneo personal, fuera de cualquier organización.

Eso es deliberado: la API nunca confirma la existencia de datos que tu token no puede leer, así que los ids no se pueden sondear. En la práctica, si estás seguro de que el torneo existe, revisa que estés usando el token de la organización que es su dueña.

Qué hacer en cada estado

const MAX_RETRIES = 3;

async function settoFetch(path, { token, attempt = 0, ...init } = {}) {
  const res = await fetch(
    `https://api.setto.io/v1${path}`,
    {
      ...init,
      headers: { ...init.headers, Authorization: `Bearer ${token}` },
    },
  );

  if (res.ok) return res.json();

  const body = await res.json().catch(() => ({}));

  const canRetry = attempt < MAX_RETRIES;
  const retry = async (waitMs) => {
    await new Promise((r) => setTimeout(r, waitMs));
    return settoFetch(path, { ...init, token, attempt: attempt + 1 });
  };

  switch (res.status) {
    case 401:
      // MISSING_API_TOKEN | INVALID_API_TOKEN | EXPIRED_API_TOKEN
      throw new Error(`SETTO credential problem: ${body.code}`);
    case 403:
      throw new Error('SETTO token lacks the required scope');
    case 404:
      return null; // quien llama decide si un miss es fatal
    case 429:
      // Respeta Retry-After, pero ríndete en lugar de dar vueltas para siempre:
      // un token siempre pasado de presupuesto giraría aquí indefinidamente.
      if (!canRetry) throw new Error('SETTO API: sigue rate limited');
      return retry(Number(res.headers.get('Retry-After') ?? 1) * 1000);
    default:
      // Solo 5xx, con backoff exponencial y jitter para que una flota de
      // clientes no vuelva toda al mismo tiempo.
      if (res.status >= 500 && canRetry) {
        return retry(2 ** attempt * 500 + Math.random() * 250);
      }
      throw new Error(`SETTO API ${res.status}: ${JSON.stringify(body)}`);
  }
}

Reintenta 429 y 5xx, y ponle un límite — aquí son tres intentos. Nunca reintentes 400, 401, 403 ni 404: la misma petición va a fallar igual, y un bucle de reintentos sobre 401 solo quema tu límite de uso.

Relacionado

En esta página