[Bug] WebSocket Streams: stream registry is global, two clients with the same stream name get each other's data
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
Research direction
Start in common/src/binance_common/websocket.py with WebSocketStreamBase: find the module-level maps global_stream_connections and global_user_stream_connections, then every place they are read or written — the subscribe filter, on, unsubscribe, and reconnect cleanup — and move them to per-instance attributes initialized in __init__. Run the self-contained aiohttp reproduction script from the issue body first to confirm the wrong output, then re-run it to check that each server gets its own SUBSCRIBE and fut_cb receives {'venue': 'futures'}. Check the repo's existing websocket tests still pass, since pooling and dedup logic depend on this registry.
Written by the indexing model from the issue text.
Description
Summary
WebSocketStreamBase keeps its stream-to-connection registry in two module-level objects (global_stream_connections, global_user_stream_connections in common/src/binance_common/websocket.py). Every client in the process shares them, keyed only by stream name.
Spot and USDⓈ-M Futures use the same stream names (btcusdt@aggTrade, btcusdt@depth, ...), and so do mainnet and testnet. When two stream clients in one process subscribe to the same name:
- the second client never sends
SUBSCRIBE, because the name is already registered; - its callback is attached to the first client's connection, so it receives the other venue's data, with no error.
Reproduce
Self-contained: one local aiohttp server with two endpoints standing in for two venues; each tags its messages with its name. binance-common==4.5.1 (same as master at 43f19a9), Python 3.13.
import asyncio, json, logging
from aiohttp import web
from binance_common.configuration import ConfigurationWebSocketStreams
from binance_common.websocket import WebSocketStreamBase, RequestStream
logging.disable(logging.CRITICAL)
subs = {"spot": [], "futures": []}
def mk(name):
async def handler(request):
ws = web.WebSocketResponse(); await ws.prepare(request)
async for msg in ws:
d = json.loads(msg.data)
if d["method"] == "SUBSCRIBE":
subs[name] += d["params"]
await ws.send_json({"id": d["id"], "result": None})
for s in d["params"]:
await ws.send_json({"stream": s, "data": {"venue": name}})
return ws
return handler
async def main():
app = web.Application()
app.router.add_get("/spot/stream", mk("spot")); app.router.add_get("/fut/stream", mk("futures"))
r = web.AppRunner(app); await r.setup(); await web.TCPSite(r, "127.0.0.1", 18766).start()
spot = WebSocketStreamBase(ConfigurationWebSocketStreams(stream_url="ws://127.0.0.1:18766/spot/stream"))
fut = WebSocketStreamBase(ConfigurationWebSocketStreams(stream_url="ws://127.0.0.1:18766/fut/stream"))
await spot.create_connection(); await fut.create_connection()
got = {"spot_cb": [], "fut_cb": []}
h1 = await RequestStream(spot, "btcusdt@aggTrade"); h1.on("message", lambda d: got["spot_cb"].append(d))
h2 = await RequestStream(fut, "btcusdt@aggTrade"); h2.on("message", lambda d: got["fut_cb"].append(d))
await asyncio.sleep(0.5)
print("SUBSCRIBE frames received by servers:", subs)
print("callback payloads:", got)
await spot.close_connection(); await fut.close_connection(); await r.cleanup()
asyncio.run(main())
Output:
SUBSCRIBE frames received by servers: {'spot': ['btcusdt@aggTrade'], 'futures': []}
callback payloads: {'spot_cb': [{'stream': 'btcusdt@aggTrade', 'data': {'venue': 'spot'}}], 'fut_cb': [{'stream': 'btcusdt@aggTrade', 'data': {'venue': 'spot'}}]}
Expected
Each server receives its own SUBSCRIBE, and fut_cb receives {'venue': 'futures'}.
Impact
A process that follows the same symbol on Spot and Futures (basis or arbitrage monitoring, for example) gets Spot trades delivered to the Futures handler. Nothing is logged, and the payload shapes are close enough that it is easy to miss.
Suggested fix
Make both maps instance attributes created in the client's __init__ and use them wherever the globals are referenced today (subscribe filter, on, unsubscribe and the reconnect cleanup). Streams would then be deduplicated per client, which is what the pooling logic needs.
- Dominant language
- Python
- Stars
- 2.9k
- Forks
- 707
- Avg merge
- 2h 17m
- Merged PRs (30d)
- 2
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from binance/binance-connector-python
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
binance/binance-connector-python#514 · 1 comment ·
-
[Bug] WebSocket API: session re-logon after reconnect is sent without apiKey and signaturePossibly taken @Amadeus-22 claimed this 2 days ago. Open
Difficulty 3/5 1-3 hours Newbie friendliness 74/100
-
[Bug] Response models declare numeric fields as strict `str`, causing silent fallback of `.data()` to raw dict (simple-earn & staking)Possibly taken @Tyagiquamar claimed this 24 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 68/100
binance/binance-connector-python#553 · 2 comments ·
-
Difficulty 5/5 Over a week Newbie friendliness 20/100
-
Difficulty 5/5 Over a week Newbie friendliness 30/100
binance/binance-connector-python#464 · 1 comment · 5 reactions ·
All issues in binance/binance-connector-python
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
spec-kitty/spec-kitty#6112 ·
Maintainers usually reply within 1 day
-
[Bug]: OpenRouter streaming chunks report the requested router name instead of the model OpenRouter servedPossibly taken @Mrun25 claimed this today. Openbug llm translation
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
BerriAI/litellm#46095 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
uberdeveloper/fastbt#29 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
andrew-blake/melcloudhome#368 ·
Maintainers usually reply within 1 day
-
lane:discover
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
SPARDiffData/differential-data-collection#33 ·
Maintainers usually reply within 1 day