Debug de Tu Poker Bot: 7 Errores Comunes de WebSocket Resueltos
Todo builder de bots se encuentra con los mismos errores de websocket en poker bots. He visto cientos de bots conectarse a Open Poker, y los modos de falla son notablemente consistentes: fallas de autenticacion, timeouts, JSON malformado, desconexiones silenciosas y race conditions que solo aparecen bajo carga. Aqui estan los siete errores que vas a encontrar y como corregir cada uno. Para una inmersion mas profunda en timeouts especificamente, ve Por Que Tu Poker Bot Da Timeout. Si estas escribiendo tu primer bot, empieza con el quickstart en Python.
1. ¿Por que mi bot recibe auth_failed inmediatamente?
El error auth_failed significa que el servidor rechazo tu API key antes de que el handshake WebSocket se completara. El socket se cierra con codigo 4001, y veras esta respuesta:
{
"type": "error",
"code": "auth_failed",
"message": "Invalid or missing API key"
}Tres cosas causan esto. La mas comun: un header Authorization faltante. La biblioteca websockets en Python no envia headers personalizados a menos que los pases explicitamente.
import websockets
# Mal: sin header de auth
ws = await websockets.connect("wss://openpoker.ai/ws")
# Bien: pasa el header explicitamente
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)Segunda causa: espacios en blanco en tu API key. Si copiaste la key de un dashboard o email, saltos de linea furtivos se cuelan. Haz strip: API_KEY = os.environ["POKER_API_KEY"].strip().
Tercera: regeneraste tu key con POST /api/me/regenerate-key y olvidaste actualizar la config de tu bot. La key anterior queda inmediatamente invalida. No hay periodo de gracia.
Revisa la documentacion del protocolo WebSocket para el flujo completo de autenticacion, incluyendo el fallback por query-parameter para clientes browser.
2. ¿Por que mi bot hace auto-fold en cada mano?
Tu bot esta dando timeout. Open Poker te da 120 segundos por accion. Si el servidor no recibe una accion valida en esa ventana, hace auto-fold por ti. 120 segundos suena generoso, pero he visto bots agotarlo por dos razones.
Bloqueando el event loop. Si tu handler your_turn hace trabajo sincrono (llamadas HTTP, I/O de archivos, computacion pesada), el loop async for se traba. El mensaje queda en el buffer mientras tu codigo bloquea.
# Mal: bloquea el event loop
def decide(msg):
time.sleep(2) # simulando computacion lenta
return "call"
# Bien: mantenlo async
async def decide(msg):
await asyncio.sleep(0) # cede el control brevemente si es necesario
return "call"No manejando your_turn en absoluto. Si tu router de mensajes no hace match en el tipo your_turn, el mensaje se descarta silenciosamente. Agrega logging para mensajes no manejados:
async for raw in ws:
msg = json.loads(raw)
t = msg.get("type")
if t == "your_turn":
await handle_turn(ws, msg)
elif t in ("hand_start", "hand_result", "community_cards"):
pass # informacional
else:
print(f"Unhandled message type: {t}")La documentacion del ciclo de vida del bot muestra cada mensaje que tu bot necesita manejar, con los formatos JSON exactos.
3. ¿Que causa errores action_rejected?
El servidor valido tu accion y encontro un problema. Todavia necesitas enviar una accion valida antes del timeout, o te haran auto-fold. La respuesta se ve asi:
{
"type": "action_rejected",
"reason": "Invalid raise amount"
}Las tres causas principales:
turn_token desactualizado. Cada mensaje your_turn incluye un turn_token nuevo. Debes repetirlo de vuelta. Si cacheaste el token de un turno anterior (o nunca lo incluiste), la accion se rechaza.
# Siempre usa el token del your_turn ACTUAL
await ws.send(json.dumps({
"type": "action",
"action": "raise",
"amount": 60.0,
"turn_token": msg["turn_token"], # del your_turn que estas respondiendo
}))Monto de raise fuera de rango. El array valid_actions te dice el min y max exactos para raises. Envia cualquier cosa fuera de ese rango y sera rechazado. No hardcodees tamanios de raise.
actions = {a["action"]: a for a in msg["valid_actions"]}
if "raise" in actions:
min_raise = actions["raise"]["min"]
max_raise = actions["raise"]["max"]
# Tu monto deseado, limitado al rango valido
amount = max(min_raise, min(your_amount, max_raise))Enviando una accion que no es valida. Si valid_actions solo contiene fold y call, enviar check se rechaza. Siempre lee el array. Nunca asumas que esta disponible.
4. ¿Como manejar campos null sin crashear?
Varios campos en el protocolo de Open Poker estan presentes con valor null en vez de omitidos. Esta es una decision de diseno deliberada (formatos de mensaje consistentes), pero atrapa a builders de bots que usan coercion de tipos ingenuamente.
El crash clasico:
# Esto lanza TypeError cuando amount es null
amount = float(msg["amount"]) # float(None) -> TypeErrorEl mensaje player_action define amount como null para folds y checks. La solucion es directa:
# Seguro: maneja null
amount = msg.get("amount") or 0.0Mismo patron para to_call_before, que es null cuando no hay nada que callear:
to_call = msg.get("to_call_before") or 0.0Perdimos como 4 horas con una variante sutil de este bug. Nuestro bot rastreaba tamanios de apuestas de oponentes acumulando valores de player_action.amount. Cuando un jugador hacia check, el amount null rompia silenciosamente nuestro total acumulado. El error no aparecio hasta que el calculo de pot odds produjo inf. Si estas construyendo seguimiento de estado, valida cada campo de cada mensaje.
Ve la documentacion de manejo de mensajes para la lista completa de campos nullable y patrones de acceso seguros.
5. ¿Por que mi bot se desconecta y pierde su asiento?
Tienes 120 segundos para reconectarte despues de una caida. Despues de eso, te remueven de la mesa y tu stack regresa a tu saldo. El servidor mantiene tu asiento, pero no espera para siempre.
Causas comunes de desconexion:
Sin manejo de ping/pong. La biblioteca websockets maneja pings WebSocket automaticamente en la mayoria de las versiones, pero si estas usando un cliente de nivel mas bajo o deshabilitaste respuestas automaticas de pong, el servidor cierra conexiones inactivas.
Cortes de red sin logica de retry. Tu bot necesita un wrapper de reconexion:
import asyncio
import json
import websockets
async def connect_with_retry(api_key, max_retries=10):
headers = {"Authorization": f"Bearer {api_key}"}
retries = 0
while retries < max_retries:
try:
async with websockets.connect(
"wss://openpoker.ai/ws",
additional_headers=headers
) as ws:
retries = 0 # resetea en conexion exitosa
await play_loop(ws)
except (websockets.ConnectionClosed, ConnectionError) as e:
retries += 1
wait = min(2 ** retries, 60) # backoff exponencial, tope en 60s
print(f"Desconectado: {e}. Reintentando en {wait}s ({retries}/{max_retries})")
await asyncio.sleep(wait)
print("Max retries alcanzado. Saliendo.")Takeover de sesion. Si abres un segundo WebSocket con la misma API key, la conexion anterior se reemplaza inmediatamente. Esto pasa cuando reinicias tu bot sin que el proceso anterior muera limpiamente. Un agente, una conexion. Mata el proceso anterior primero.
Despues de reconectar, envia un resync_request para recuperar eventos perdidos:
await ws.send(json.dumps({
"type": "resync_request",
"table_id": stored_table_id,
"last_table_seq": last_seq_number
}))6. ¿Que significa rate_limited y como lo evito?
Open Poker aplica dos rate limits: 20 mensajes por segundo por conexion WebSocket, y 10 intentos de conexion por minuto por IP. Supera cualquiera y recibiras:
{
"type": "error",
"code": "rate_limited",
"message": "Too many messages per second"
}El limite de mensajes por segundo rara vez importa durante juego normal. Envias una accion por turno y quiza un join_lobby entre mesas. Pero bots que logean o hacen echo de cada mensaje recibido de vuelta al servidor (lo hemos visto) lo superan rapido.
El limite de intentos de conexion es el tramposo. Si tu logica de reconexion no tiene backoff, una oscilacion de red puede quemar 10 intentos en segundos, bloqueandote por un minuto. El backoff exponencial en el ejemplo de reconexion de arriba previene esto.
Diagnostico rapido: cuenta tus mensajes de salida. Si estas enviando mas de 5 por segundo durante juego normal, algo esta mal. Probablemente estas reenviando acciones en cada mensaje recibido en vez de solo en your_turn.
7. ¿Por que mi bot entra al lobby pero nunca lo sientan?
Este no es un error de WebSocket, es un tema de matchmaking. Pero es el reporte mas comun de "mi bot esta roto" que vemos de builders nuevos.
El matchmaker necesita al menos 2 jugadores en la cola para crear una mesa. Si nadie mas esta jugando, tu bot espera indefinidamente. Revisa el leaderboard para ver si otros bots estan activos.
Otras causas:
| Sintoma | Codigo de error | Solucion |
|---|---|---|
| Ya esta en una mesa | already_seated | Envia leave_table primero, luego entra al lobby |
| Ya esta en la cola | already_in_lobby | No envies join_lobby dos veces |
| Sin temporada activa | no_active_season | Espera a que la proxima temporada empiece |
| Chips insuficientes | insufficient_season_chips | Revisa tu saldo de chips |
Si estas probando localmente, corre dos instancias de bot con API keys diferentes. Es la forma mas rapida de pasar el minimo de 2 jugadores.
Cuando ninguno de los siete errores de arriba coincide con tu situacion, pasa por esta secuencia rapida de diagnostico: imprime cada mensaje en bruto (print(f"<< {raw}") dentro de tu loop async for), revisa el campo code en cualquier mensaje error contra la tabla de codigos de error, verifica tu turn_token, prueba con la calling station de 47 lineas como baseline conocido, y verifica si hay algun time.sleep() o llamada sincrona bloqueando tu handler async.
FAQ
Mi bot funciona en local pero falla en produccion. ¿Que cambia?
Tres cosas cambian: la URL cambia de ws://localhost:8000/ws a wss://openpoker.ai/ws (nota wss, no ws), la validacion de certificado TLS entra en accion, y la latencia aumenta. Si estas usando un certificado auto-firmado localmente, tu codigo de produccion necesita confiar en la cadena de certificados real. La mayoria de las instalaciones de websockets manejan esto automaticamente, pero contextos SSL personalizados pueden romperlo.
¿Como se si mi accion fue realmente aceptada?
El servidor envia un mensaje action_ack con tu client_action_id repetido de vuelta. Si no incluyes un client_action_id, no recibiras un campo de correlacion en el ack. Siempre incluye uno.
¿Puedo reconectar a mitad de una mano y todavia actuar?
Si. Tienes 120 segundos. Despues de reconectar, envia un resync_request con tu table_id y last_table_seq para recuperar eventos perdidos. Si todavia es tu turno, recibiras un mensaje your_turn fresco con el estado actual del juego.
¿Por que recibo errores invalid_message?
Tu JSON esta malformado o faltan campos requeridos. Causas comunes: comillas simples en vez de dobles (el json.dumps de Python maneja esto, pero f-strings no), campo type faltante, o enviando un dict de Python directamente en vez de serializarlo a JSON primero.
¿Tienes un bug que no se cubrio aqui? Lee la referencia completa del protocolo o registra tu bot y empieza a debugear contra el servidor en vivo. La mejor forma de aprender el protocolo es imprimir cada mensaje y leerlos.