Streaming
wss wss://ws.xsignalfeed.com/v1/stream
Verbinden
Der Stream nimmt Ihren Schlüssel auf dieselben drei Arten wie REST entgegen. Von einem Server aus verwenden Sie einen Header; den URL-Parameter nur dort, wo sich keine Header setzen lassen. Kommt der Schlüssel nicht an oder wird er nicht akzeptiert, öffnet sich die Verbindung und schließt sofort mit 4001; der Schließgrund nennt die Ursache.
| So senden Sie den Schlüssel | Verwenden, wenn |
|---|---|
Authorization: Bearer xsf_… | Empfohlen für Server; derselbe Header wie bei REST |
X-API-Key: xsf_… | Gleichwertig zum obigen |
?api_key=xsf_… | Browser und Clients, die beim Handshake keine Header setzen können. Der Schlüssel wird Teil der URL, halten Sie solche URLs daher aus Logs heraus |
import websockets async with websockets.connect("wss://ws.xsignalfeed.com/v1/stream", additional_headers={"Authorization": "Bearer xsf_your_key"}) as ws: print(await ws.recv()) # {"t": "system.hello", ...}
| Query-Parameter | Bedeutung |
|---|---|
| api_key | Ihr Schlüssel, wenn Sie den Header nicht setzen können |
| last_id | Nach dieser sid fortsetzen. Wird stillschweigend ignoriert, wenn der Schlüssel die Berechtigung replay nicht hat |
import asyncio, json, websockets async def main(): url = "wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key" async with websockets.connect(url) as ws: async for raw in ws: ev = json.loads(raw) if ev["t"].startswith("system."): continue print(ev["t"], ev["target"]["screen_name"], ev["d"].get("text", "")[:60]) asyncio.run(main())
import WebSocket from "ws"; const ws = new WebSocket("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key"); ws.on("message", (raw) => { const ev = JSON.parse(raw); if (ev.t.startsWith("system.")) return; console.log(ev.t, ev.target.screen_name, (ev.d.text ?? "").slice(0, 60)); });
c, _, err := websocket.DefaultDialer.Dial("wss://ws.xsignalfeed.com/v1/stream?api_key=xsf_your_key", nil) for { _, raw, err := c.ReadMessage() if err != nil { break } var ev Event json.Unmarshal(raw, &ev) if strings.HasPrefix(ev.T, "system.") { continue } log.Println(ev.T, ev.Target.ScreenName) }
Frame
Drei Frames stammen von uns statt von X:
| Frame | Wann |
|---|---|
system.hello | Erster Frame. targets ist die Zahl der Konten, die dieser Schlüssel in diesem Moment überwacht (später hinzugefügte Konten kommen ohne neue Verbindung dazu); replay_from gibt die last_id zurück, mit der Sie sich neu verbunden haben, und ist null, wenn Sie keine gesendet haben – dann wird nichts nachgeliefert |
system.ping | Wird bei Stille als Keepalive gesendet, damit untätige Proxys den Socket nicht schließen |
system.error | Mit der Verbindung stimmt etwas nicht; ein Close folgt |
Close-Code
Der Handshake gelingt immer, und der Close folgt unmittelbar, damit Ihre Client-Bibliothek Code und Grund sieht statt eines nackten HTTP-Fehlers.
| Close-Code | Bedeutung |
|---|---|
1012 | Neustart des Dienstes während einer Wartung. Verbinden Sie sich mit Backoff neu und übergeben Sie die letzte verarbeitete sid als ?last_id=: Die Ereignisse der Lücke werden nachgeliefert, nichts geht verloren |
4001 | Der Schlüssel wurde nicht akzeptiert. Der Schließgrund nennt die Ursache: missing_api_key (kein Schlüssel kam bei uns an: prüfen Sie den Header-Namen oder ob Ihr Proxy bzw. Ihre Client-Bibliothek ihn weiterreicht), invalid_api_key (kein solcher Schlüssel: Tippfehler oder ein durch Rotation ersetztes Secret), key_revoked (der Schlüssel wurde widerrufen) |
4002 | Zu viele gleichzeitige Verbindungen für diesen Schlüssel; ein system.error-Frame geht dem Close voraus |
4003 | Dem Schlüssel fehlt die Berechtigung stream |
Richtig neu verbinden
Schreiben Sie das einmal richtig, nicht die naive Version. Die beiden Dinge, auf die es ankommt, sind exponentielles Backoff und das Persistieren von sid an einem Ort, der einen Prozessneustart überlebt.
import asyncio, json, pathlib, random, websockets STATE = pathlib.Path("xsf.cursor") # survives restarts; a DB row is better still async def run(key, handle): backoff = 1 while True: last = STATE.read_text().strip() if STATE.exists() else None url = f"wss://ws.xsignalfeed.com/v1/stream?api_key={key}" + (f"&last_id={last}" if last else "") try: async with websockets.connect(url, ping_interval=20, max_size=2**22) as ws: backoff = 1 async for raw in ws: ev = json.loads(raw) if ev["t"].startswith("system."): continue handle(ev) # must be idempotent: dedupe on ev["id"] if ev.get("sid"): STATE.write_text(ev["sid"]) # only after handling except Exception as e: print("disconnected:", e) await asyncio.sleep(backoff + random.random()) # jitter, or every client returns at once backoff = min(60, backoff * 2)