Skip to content
[OPEN_POKER]

Debug de ton Poker Bot : 7 erreurs WebSocket courantes corrigees

JJoão Carvalho||10 min read

Tout builder de bots rencontre les memes erreurs websocket sur les poker bots. J'ai regarde des centaines de bots se connecter a Open Poker, et les modes de defaillance sont remarquablement consistants : echecs d'auth, timeouts, JSON malformate, deconnexions silencieuses et race conditions qui n'apparaissent que sous charge. Voici les sept erreurs que tu vas rencontrer et comment corriger chacune. Pour une exploration plus approfondie des timeouts specifiquement, voir Pourquoi ton Poker Bot fait Timeout. Si tu ecris ton premier bot, commence par le quickstart Python.

1. Pourquoi mon bot recoit auth_failed immediatement ?

L'erreur auth_failed signifie que le serveur a rejete ta cle API avant que le handshake WebSocket ne soit termine. Le socket se ferme avec le code 4001, et tu verras cette reponse :

{
  "type": "error",
  "code": "auth_failed",
  "message": "Invalid or missing API key"
}

Trois choses causent ca. La plus courante : un header Authorization manquant. La bibliotheque websockets en Python n'envoie pas de headers personnalises sauf si tu les passes explicitement.

import websockets
 
# Faux : pas de header d'auth
ws = await websockets.connect("wss://openpoker.ai/ws")
 
# Correct : passe le header explicitement
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)

Deuxieme cause : des espaces dans ta cle API. Si tu as copie la cle depuis un dashboard ou un email, des retours a la ligne sournois se glissent. Fais un strip : API_KEY = os.environ["POKER_API_KEY"].strip().

Troisieme : tu as regenere ta cle avec POST /api/me/regenerate-key et tu as oublie de mettre a jour la config de ton bot. L'ancienne cle est immediatement invalide. Il n'y a pas de periode de grace.

Consulte la documentation du protocole WebSocket pour le flux d'authentification complet, y compris le fallback par query-parameter pour les clients navigateur.

2. Pourquoi mon bot fait auto-fold a chaque main ?

Ton bot fait timeout. Open Poker te donne 120 secondes par action. Si le serveur ne recoit pas d'action valide dans cette fenetre, il fait auto-fold pour toi. 120 secondes semble genereux, mais j'ai vu des bots exploser ce delai pour deux raisons.

Bloquer l'event loop. Si ton handler your_turn fait du travail synchrone (appels HTTP, I/O fichier, calcul lourd), la boucle async for bloque. Le message reste dans le buffer pendant que ton code bloque.

# Mauvais : bloque l'event loop
def decide(msg):
    time.sleep(2)  # simule un calcul lent
    return "call"
 
# Bon : garde-le async
async def decide(msg):
    await asyncio.sleep(0)  # cede le controle brievement si necessaire
    return "call"

Ne pas gerer your_turn du tout. Si ton routeur de messages ne matche pas le type your_turn, le message est silencieusement ignore. Ajoute du logging pour les messages non geres :

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  # informationnel
    else:
        print(f"Unhandled message type: {t}")

La documentation du cycle de vie du bot montre chaque message que ton bot doit gerer, avec les formats JSON exacts.

3. Qu'est-ce qui cause les erreurs action_rejected ?

Le serveur a valide ton action et a trouve un probleme. Tu dois toujours envoyer une action valide avant le timeout, sinon tu subiras un auto-fold. La reponse ressemble a ca :

{
  "type": "action_rejected",
  "reason": "Invalid raise amount"
}

Les trois causes principales :

turn_token perime. Chaque message your_turn inclut un turn_token frais. Tu dois le renvoyer. Si tu as cache le token d'un tour precedent (ou ne l'as jamais inclus), l'action est rejetee.

# Toujours utiliser le token du your_turn ACTUEL
await ws.send(json.dumps({
    "type": "action",
    "action": "raise",
    "amount": 60.0,
    "turn_token": msg["turn_token"],  # du your_turn auquel tu reponds
}))

Montant de raise hors limites. Le tableau valid_actions te donne les min et max exacts pour les raises. Envoie quoi que ce soit en dehors de cette plage et c'est rejete. Ne hardcode pas les tailles 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"]
    # Ton montant desire, limite a la plage valide
    amount = max(min_raise, min(your_amount, max_raise))

Envoyer une action qui n'est pas valide. Si valid_actions ne contient que fold et call, envoyer check est rejete. Lis toujours le tableau. Ne presume jamais de ce qui est disponible.

4. Comment gerer les champs null sans crasher ?

Plusieurs champs dans le protocole d'Open Poker sont presents avec la valeur null plutot qu'omis. C'est un choix de design delibere (formats de messages consistants), mais ca piege les builders de bots qui utilisent une coercion de types naive.

Le crash classique :

# Ca lance TypeError quand amount est null
amount = float(msg["amount"])  # float(None) -> TypeError

Le message player_action definit amount a null pour les folds et checks. La correction est simple :

# Sur : gere null
amount = msg.get("amount") or 0.0

Meme pattern pour to_call_before, qui est null quand il n'y a rien a caller :

to_call = msg.get("to_call_before") or 0.0

On a perdu environ 4 heures sur une variante subtile de ce bug. Notre bot suivait les tailles de mises des adversaires en accumulant les valeurs de player_action.amount. Quand un joueur checkait, le amount null cassait silencieusement notre total courant. L'erreur n'est apparue que quand le calcul de pot odds a produit inf. Si tu construis du state tracking, valide chaque champ de chaque message.

Voir la documentation de gestion des messages pour la liste complete des champs nullable et les patterns d'acces surs.

5. Pourquoi mon bot se deconnecte et perd sa place ?

Tu as 120 secondes pour te reconnecter apres une coupure. Apres ca, tu es retire de la table et ton stack retourne a ton solde. Le serveur garde ta place, mais il n'attend pas eternellement.

Causes courantes de deconnexion :

Pas de gestion ping/pong. La bibliotheque websockets gere les pings WebSocket automatiquement dans la plupart des versions, mais si tu utilises un client de bas niveau ou as desactive les reponses pong automatiques, le serveur ferme les connexions inactives.

Coupures reseau sans logique de retry. Ton bot a besoin d'un wrapper de reconnexion :

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  # reset en cas de connexion reussie
                await play_loop(ws)
        except (websockets.ConnectionClosed, ConnectionError) as e:
            retries += 1
            wait = min(2 ** retries, 60)  # backoff exponentiel, plafond 60s
            print(f"Deconnecte : {e}. Nouvelle tentative dans {wait}s ({retries}/{max_retries})")
            await asyncio.sleep(wait)
    print("Max retries atteint. Arret.")

Takeover de session. Si tu ouvres un deuxieme WebSocket avec la meme cle API, l'ancienne connexion est immediatement remplacee. Ca arrive quand tu redemarres ton bot sans que le processus precedent ne se termine proprement. Un agent, une connexion. Termine l'ancien processus d'abord.

Apres la reconnexion, envoie un resync_request pour recuperer les evenements manques :

await ws.send(json.dumps({
    "type": "resync_request",
    "table_id": stored_table_id,
    "last_table_seq": last_seq_number
}))

6. Que signifie rate_limited et comment l'eviter ?

Open Poker applique deux rate limits : 20 messages par seconde par connexion WebSocket, et 10 tentatives de connexion par minute par IP. Depasse l'un ou l'autre et tu recevras :

{
  "type": "error",
  "code": "rate_limited",
  "message": "Too many messages per second"
}

La limite de messages par seconde compte rarement pendant le jeu normal. Tu envoies une action par tour et peut-etre un join_lobby entre les tables. Mais les bots qui loguent ou renvoient chaque message recu au serveur (on a vu ca) la depassent vite.

La limite de tentatives de connexion est la sournoise. Si ta logique de reconnexion n'a pas de backoff, une instabilite reseau peut bruler 10 tentatives en quelques secondes, te bloquant pendant une minute. Le backoff exponentiel dans l'exemple de reconnexion ci-dessus empeche ca.

Diagnostic rapide : compte tes messages sortants. Si tu en envoies plus de 5 par seconde pendant le jeu normal, quelque chose ne va pas. Tu renvoies probablement des actions a chaque message recu au lieu de seulement sur your_turn.

7. Pourquoi mon bot rejoint le lobby mais n'est jamais place ?

Ce n'est pas une erreur WebSocket, c'est un probleme de matchmaking. Mais c'est le rapport "mon bot est casse" le plus courant qu'on recoit des nouveaux builders.

Le matchmaker a besoin d'au moins 2 joueurs dans la file pour creer une table. Si personne d'autre ne joue, ton bot attend indefiniment. Verifie le leaderboard pour voir si d'autres bots sont actifs.

Autres causes :

SymptomeCode d'erreurCorrection
Deja a une tablealready_seatedEnvoie d'abord leave_table, puis rejoins le lobby
Deja dans la filealready_in_lobbyN'envoie pas join_lobby deux fois
Pas de saison activeno_active_seasonAttends le debut de la prochaine saison
Pas assez de chipsinsufficient_season_chipsVerifie ton solde de chips

Si tu testes en local, lance deux instances de bot avec des cles API differentes. C'est le moyen le plus rapide de depasser le minimum de 2 joueurs.

Quand aucune des sept erreurs ci-dessus ne correspond a ta situation, suis cette sequence de diagnostic rapide : affiche chaque message brut (print(f"<< {raw}") dans ta boucle async for), verifie le champ code dans tout message error contre la table des codes d'erreur, verifie ton turn_token, teste avec la calling station de 47 lignes comme baseline connue, et verifie s'il y a des time.sleep() ou appels synchrones qui bloquent ton handler async.

FAQ

Mon bot fonctionne en local mais echoue en production. Qu'est-ce qui change ? Trois choses changent : l'URL passe de ws://localhost:8000/ws a wss://openpoker.ai/ws (note wss, pas ws), la validation du certificat TLS entre en jeu, et la latence augmente. Si tu utilises un certificat auto-signe en local, ton code de production doit faire confiance a la vraie chaine de certificats. La plupart des installations websockets gerent ca automatiquement, mais les contextes SSL personnalises peuvent casser.

Comment savoir si mon action a ete reellement acceptee ? Le serveur envoie un message action_ack avec ton client_action_id renvoye. Si tu n'inclus pas de client_action_id, tu ne recevras pas de champ de correlation dans l'ack. Inclus-en toujours un.

Puis-je me reconnecter en pleine main et toujours agir ? Oui. Tu as 120 secondes. Apres la reconnexion, envoie un resync_request avec ton table_id et last_table_seq pour recuperer les evenements manques. Si c'est toujours ton tour, tu recevras un message your_turn frais avec l'etat actuel du jeu.

Pourquoi est-ce que je recois des erreurs invalid_message ? Ton JSON est malformate ou il manque des champs requis. Causes courantes : guillemets simples au lieu de doubles (le json.dumps de Python gere ca, mais les f-strings non), champ type manquant, ou envoi d'un dict Python directement au lieu de le serialiser en JSON d'abord.


Tu as un bug qui n'est pas couvert ici ? Lis la reference complete du protocole ou enregistre ton bot et commence a debugger contre le serveur live. La meilleure facon d'apprendre le protocole est d'afficher chaque message et de les lire.

Continuer la lecture