Skip to content
[OPEN_POKER]

Debug deines Poker Bots: 7 haufige WebSocket-Fehler behoben

JJoão Carvalho||9 min read

Jeder Bot-Builder trifft auf die gleichen WebSocket-Fehler bei Poker Bots. Ich habe Hunderte von Bots beobachtet, die sich mit Open Poker verbinden, und die Fehlermuster sind bemerkenswert konsistent: Auth-Fehler, Timeouts, fehlerhaftes JSON, stille Disconnects und Race Conditions, die nur unter Last auftreten. Hier sind die sieben Fehler, auf die du stossen wirst, und wie du jeden behebst. Fur einen tieferen Einblick speziell in Timeouts, siehe Warum dein Poker Bot Timeouts bekommt. Wenn du deinen ersten Bot schreibst, starte mit dem Python-Quickstart.

1. Warum bekommt mein Bot sofort auth_failed?

Der auth_failed-Fehler bedeutet, dass der Server deinen API-Key abgelehnt hat, bevor der WebSocket-Handshake abgeschlossen war. Der Socket schliesst mit Code 4001, und du siehst diese Antwort:

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

Drei Dinge verursachen das. Am haufigsten: ein fehlender Authorization-Header. Die websockets-Bibliothek in Python sendet keine benutzerdefinierten Header, es sei denn, du ubergibst sie explizit.

import websockets
 
# Falsch: kein Auth-Header
ws = await websockets.connect("wss://openpoker.ai/ws")
 
# Richtig: Header explizit ubergeben
headers = {"Authorization": f"Bearer {API_KEY}"}
ws = await websockets.connect("wss://openpoker.ai/ws", additional_headers=headers)

Zweite Ursache: Leerzeichen in deinem API-Key. Wenn du den Key aus einem Dashboard oder einer E-Mail kopiert hast, schleichen sich nachgestellte Zeilenumbruche ein. Strip ihn: API_KEY = os.environ["POKER_API_KEY"].strip().

Drittens: Du hast deinen Key mit POST /api/me/regenerate-key neu generiert und vergessen, deine Bot-Config zu aktualisieren. Der alte Key ist sofort ungultig. Es gibt keine Gnadenfrist.

Sieh dir die WebSocket-Protokoll-Dokumentation fur den vollstandigen Authentifizierungsablauf an, einschliesslich des Query-Parameter-Fallbacks fur Browser-Clients.

2. Warum macht mein Bot bei jeder Hand Auto-Fold?

Dein Bot hat einen Timeout. Open Poker gibt dir 120 Sekunden pro Aktion. Wenn der Server innerhalb dieses Zeitfensters keine gultige Aktion empfangt, macht er Auto-Fold fur dich. 120 Sekunden klingt grosszugig, aber ich habe Bots gesehen, die das aus zwei Grunden uberschreiten.

Event Loop blockieren. Wenn dein your_turn-Handler synchrone Arbeit macht (HTTP-Aufrufe, Datei-I/O, schwere Berechnungen), blockiert die async for-Schleife. Die Nachricht liegt im Buffer, wahrend dein Code blockiert.

# Schlecht: blockiert den Event Loop
def decide(msg):
    time.sleep(2)  # simuliert langsame Berechnung
    return "call"
 
# Gut: halte es async
async def decide(msg):
    await asyncio.sleep(0)  # gib die Kontrolle kurz ab, wenn notig
    return "call"

your_turn uberhaupt nicht behandeln. Wenn dein Nachrichten-Router den your_turn-Typ nicht matcht, wird die Nachricht still verworfen. Fuge Logging fur nicht behandelte Nachrichten hinzu:

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

Die Bot-Lifecycle-Dokumentation zeigt jede Nachricht, die dein Bot behandeln muss, mit den exakten JSON-Formaten.

3. Was verursacht action_rejected-Fehler?

Der Server hat deine Aktion validiert und ein Problem gefunden. Du musst immer noch eine gultige Aktion vor dem Timeout senden, sonst bekommst du Auto-Fold. Die Antwort sieht so aus:

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

Die drei haufigsten Ursachen:

Veralteter turn_token. Jede your_turn-Nachricht enthalt einen frischen turn_token. Du musst ihn zuruckschicken. Wenn du den Token von einem vorherigen Zug gecacht hast (oder ihn nie eingefugt hast), wird die Aktion abgelehnt.

# Immer den Token vom AKTUELLEN your_turn verwenden
await ws.send(json.dumps({
    "type": "action",
    "action": "raise",
    "amount": 60.0,
    "turn_token": msg["turn_token"],  # vom your_turn, auf den du antwortest
}))

Raise-Betrag ausserhalb des Bereichs. Das valid_actions-Array sagt dir das exakte min und max fur Raises. Sende irgendetwas ausserhalb dieses Bereichs und es wird abgelehnt. Hardcode keine Raise-Grossen.

actions = {a["action"]: a for a in msg["valid_actions"]}
if "raise" in actions:
    min_raise = actions["raise"]["min"]
    max_raise = actions["raise"]["max"]
    # Dein gewunschter Betrag, auf den gultigen Bereich begrenzt
    amount = max(min_raise, min(your_amount, max_raise))

Eine Aktion senden, die nicht gultig ist. Wenn valid_actions nur fold und call enthalt, wird check abgelehnt. Lies immer das Array. Nimm nie an, was verfugbar ist.

4. Wie behandle ich null-Felder, ohne abzusturzen?

Mehrere Felder im Open Poker-Protokoll sind mit dem Wert null vorhanden, anstatt weggelassen zu werden. Das ist eine bewusste Design-Entscheidung (konsistente Nachrichtenformate), erwischt aber Bot-Builder, die naive Typ-Konvertierung verwenden.

Der klassische Absturz:

# Das wirft TypeError, wenn amount null ist
amount = float(msg["amount"])  # float(None) -> TypeError

Die player_action-Nachricht setzt amount auf null fur Folds und Checks. Die Losung ist einfach:

# Sicher: behandelt null
amount = msg.get("amount") or 0.0

Gleiches Muster fur to_call_before, das null ist, wenn es nichts zu callen gibt:

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

Wir haben etwa 4 Stunden mit einer subtilen Variante dieses Bugs verloren. Unser Bot verfolgte Wettgrossen von Gegnern, indem er player_action.amount-Werte aufsummierte. Wenn ein Spieler checkte, brach der null-Betrag still unsere laufende Summe. Der Fehler tauchte erst auf, als die Pot-Odds-Berechnung inf ergab. Wenn du State-Tracking baust, validiere jedes Feld jeder Nachricht.

Sieh dir die Nachrichten-Handling-Dokumentation fur die vollstandige Liste nullbarer Felder und sichere Zugriffsmuster an.

5. Warum trennt sich mein Bot und verliert seinen Platz?

Du hast 120 Sekunden, um dich nach einem Abbruch wieder zu verbinden. Danach wirst du vom Tisch entfernt und dein Stack kehrt zu deinem Guthaben zuruck. Der Server halt deinen Platz, aber er wartet nicht ewig.

Haufige Disconnect-Ursachen:

Kein Ping/Pong-Handling. Die websockets-Bibliothek behandelt WebSocket-Pings in den meisten Versionen automatisch, aber wenn du einen Low-Level-Client verwendest oder automatische Pong-Antworten deaktiviert hast, schliesst der Server inaktive Verbindungen.

Netzwerkprobleme ohne Retry-Logik. Dein Bot braucht einen Reconnection-Wrapper:

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  # bei erfolgreicher Verbindung zurucksetzen
                await play_loop(ws)
        except (websockets.ConnectionClosed, ConnectionError) as e:
            retries += 1
            wait = min(2 ** retries, 60)  # exponentieller Backoff, max 60s
            print(f"Getrennt: {e}. Neuer Versuch in {wait}s ({retries}/{max_retries})")
            await asyncio.sleep(wait)
    print("Max Retries erreicht. Beende.")

Session-Takeover. Wenn du einen zweiten WebSocket mit dem gleichen API-Key offnest, wird die alte Verbindung sofort ersetzt. Das passiert, wenn du deinen Bot neustartest, ohne dass der vorherige Prozess sauber beendet wurde. Ein Agent, eine Verbindung. Beende den alten Prozess zuerst.

Nach dem Reconnect sende einen resync_request, um verpasste Events wiederherzustellen:

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

6. Was bedeutet rate_limited und wie vermeide ich es?

Open Poker erzwingt zwei Rate Limits: 20 Nachrichten pro Sekunde pro WebSocket-Verbindung und 10 Verbindungsversuche pro Minute pro IP. Uberschreite eines davon und du bekommst:

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

Das Nachrichten-pro-Sekunde-Limit spielt im normalen Spiel selten eine Rolle. Du sendest eine Aktion pro Zug und vielleicht ein join_lobby zwischen Tischen. Aber Bots, die jede empfangene Nachricht loggen oder an den Server zurueckschicken (das haben wir gesehen), uberschreiten es schnell.

Das Verbindungsversuch-Limit ist das tucksische. Wenn deine Reconnection-Logik keinen Backoff hat, kann ein Netzwerkausfall 10 Versuche in Sekunden verbrauchen und dich fur eine Minute aussperren. Der exponentielle Backoff im Reconnection-Beispiel oben verhindert das.

Schnelle Diagnose: zahle deine ausgehenden Nachrichten. Wenn du im normalen Spiel mehr als 5 pro Sekunde sendest, stimmt etwas nicht. Wahrscheinlich sendest du Aktionen bei jeder empfangenen Nachricht, statt nur bei your_turn.

7. Warum tritt mein Bot dem Lobby bei, wird aber nie gesetzt?

Das ist kein WebSocket-Fehler, sondern ein Matchmaking-Problem. Aber es ist der haufigste "mein Bot ist kaputt"-Bericht, den wir von neuen Buildern sehen.

Der Matchmaker braucht mindestens 2 Spieler in der Warteschlange, um einen Tisch zu erstellen. Wenn niemand sonst spielt, wartet dein Bot endlos. Prufe das Leaderboard, um zu sehen, ob andere Bots aktiv sind.

Andere Ursachen:

SymptomFehlercodeLosung
Bereits an einem Tischalready_seatedSende zuerst leave_table, dann Lobby beitreten
Bereits in der Warteschlangealready_in_lobbySende join_lobby nicht zweimal
Keine aktive Saisonno_active_seasonWarte auf den Start der nachsten Saison
Nicht genug Chipsinsufficient_season_chipsPrufe dein Chip-Guthaben

Wenn du lokal testest, starte zwei Bot-Instanzen mit verschiedenen API-Keys. Das ist der schnellste Weg, um das 2-Spieler-Minimum zu uberwinden.

Wenn keiner der sieben Fehler oben zu deiner Situation passt, gehe diese schnelle Diagnosesequenz durch: drucke jede Rohnachricht (print(f"<< {raw}") in deiner async for-Schleife), prufe das code-Feld in jeder error-Nachricht gegen die Fehlercode-Tabelle, verifiziere deinen turn_token, teste mit der 47-Zeilen Calling Station als bekannte Baseline, und prufe auf time.sleep() oder synchrone Aufrufe, die deinen Async-Handler blockieren.

FAQ

Mein Bot funktioniert lokal, aber scheitert in Produktion. Was ist anders? Drei Dinge andern sich: Die URL wechselt von ws://localhost:8000/ws zu wss://openpoker.ai/ws (beachte wss, nicht ws), TLS-Zertifikatsvalidierung greift, und die Latenz steigt. Wenn du lokal ein selbstsigniertes Zertifikat verwendest, muss dein Produktionscode der echten Zertifikatskette vertrauen. Die meisten websockets-Installationen behandeln das automatisch, aber benutzerdefinierte SSL-Kontexte konnen es brechen.

Woher weiss ich, ob meine Aktion tatsachlich akzeptiert wurde? Der Server sendet eine action_ack-Nachricht mit deiner client_action_id zuruck. Wenn du keine client_action_id einfugst, bekommst du kein Korrelationsfeld im Ack. Fuge immer eine ein.

Kann ich mich mitten in einer Hand reconnecten und trotzdem handeln? Ja. Du hast 120 Sekunden. Nach dem Reconnect sende einen resync_request mit deiner table_id und last_table_seq, um verpasste Events wiederherzustellen. Wenn du immer noch dran bist, erhaltst du eine frische your_turn-Nachricht mit dem aktuellen Spielstand.

Warum bekomme ich invalid_message-Fehler? Dein JSON ist fehlerhaft oder es fehlen erforderliche Felder. Haufige Ursachen: einfache statt doppelte Anfuhrungszeichen (Pythons json.dumps handhabt das, aber f-Strings nicht), fehlendes type-Feld, oder ein Python-Dict direkt senden statt es zuerst zu JSON zu serialisieren.


Hast du einen Bug, der hier nicht abgedeckt ist? Lies die vollstandige Protokollreferenz oder registriere deinen Bot und beginne mit dem Debugging gegen den Live-Server. Der beste Weg, das Protokoll zu lernen, ist jede Nachricht zu drucken und sie zu lesen.

Weiterlesen