From 541a126683e8d130710e6a0ee4dd38fe078ff787 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 1 Jun 2026 19:28:29 +0100 Subject: [PATCH 1/3] fix(futures-ws): reject mixed-category combined streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Binance's USDⓈ-M futures WebSocket split (2026-03-06) routes streams to separate /public, /market and /private endpoints and will not push cross-category streams on a single connection. futuresSubscribe() picked the routing category from streams[0] only, so a combined subscription mixing e.g. @aggTrade (market) and @depth (public) routed everything to one endpoint and silently dropped data for the other category. Validate that every stream in a combined subscription shares the same category and throw a descriptive error otherwise, so callers fail loudly and subscribe per category (also Binance's recommendation). All built-in helpers already build homogeneous arrays, so none are affected. --- src/node-binance-api.ts | 8 ++++++++ tests/ws-endpoints-migration.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/node-binance-api.ts b/src/node-binance-api.ts index 05c47403..ebf14ce2 100644 --- a/src/node-binance-api.ts +++ b/src/node-binance-api.ts @@ -1862,7 +1862,15 @@ export default class Binance { const httpsproxy = this.getHttpsProxy(); let socksproxy = this.getSocksProxy(); const queryParams = streams.join('/'); + // Binance routes USDⓈ-M futures streams to separate endpoints by category + // (/public, /market, /private) and will not push cross-category streams on a + // single connection. Reject mixed-category combos so they fail loudly instead + // of silently dropping data — subscribe to each category on its own connection. const category = this.classifyFuturesStream(streams[0]); + const mismatch = streams.find(s => this.classifyFuturesStream(s) !== category); + if (mismatch) { + throw new Error(`futuresSubscribe: cannot combine '${category}' stream "${streams[0]}" with '${this.classifyFuturesStream(mismatch)}' stream "${mismatch}" on one connection. Binance routes futures streams to separate /public, /market and /private endpoints; subscribe to each category separately.`); + } const baseUrl = this.getFStreamUrl(category); let ws: any = undefined; if (socksproxy) { diff --git a/tests/ws-endpoints-migration.test.ts b/tests/ws-endpoints-migration.test.ts index cd8b3dda..2875f047 100644 --- a/tests/ws-endpoints-migration.test.ts +++ b/tests/ws-endpoints-migration.test.ts @@ -152,6 +152,34 @@ describe('Private stream URL uses query params for listenKey', function () { }); }); +describe('futuresSubscribe rejects mixed-category combined streams', function () { + const listenKey = 'pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a1a65a1a5s61cv6a81va65sd'; + + it('throws when mixing market and public streams', function () { + assert.throws( + () => binance.futuresSubscribe(['btcusdt@aggTrade', 'btcusdt@depth'], () => { }), + /cannot combine/ + ); + }); + + it('throws when mixing public and private streams', function () { + assert.throws( + () => binance.futuresSubscribe(['btcusdt@bookTicker', listenKey], () => { }), + /cannot combine/ + ); + }); + + it('names both offending categories in the error message', function () { + try { + binance.futuresSubscribe(['btcusdt@markPrice', 'btcusdt@bookTicker'], () => { }); + assert.fail('expected futuresSubscribe to throw'); + } catch (e) { + assert.match(e.message, /market/); + assert.match(e.message, /public/); + } + }); +}); + describe('Live: production market stream (aggTrade via /market/)', function () { let trade; let cnt = 0; From 4fef074d15beec61b62aae55988379b87adc5aea Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 1 Jun 2026 19:54:59 +0100 Subject: [PATCH 2/3] ci: run futures WS endpoint migration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ws-endpoints-migration test file (added in #985) was never wired into any npm script or CI step, so its assertions — including the new mixed-category guard — never gated PRs. Add a `ws-tests-migration` script that runs only the hermetic (non-"Live:") describes, no network or API keys required, and invoke it from the CI workflow. --- .github/workflows/js.yml | 2 ++ package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 6a43a9ae..5ec824be 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -33,6 +33,8 @@ jobs: run: npm run ts-test-static - name: Static Tests (JS CJS) run: npm run static-test + - name: Futures WS endpoint migration tests + run: npm run ws-tests-migration - name: Live Tests (TS ESM) run: npm run ts-test-live - name: CJS test diff --git a/package.json b/package.json index 056c6dbc..feedae66 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "ws-tests": "mocha ./tests/binance-class-ws.test.ts", "ws-tests-spot": "mocha ./tests/binance-ws-spot.test.ts --exit", "ws-tests-futures": "mocha ./tests/binance-ws-futures.test.ts --exit", + "ws-tests-migration": "mocha ./tests/ws-endpoints-migration.test.ts --fgrep \"Live:\" --invert --exit", "ws-api-userdata-tests": "mocha ./tests/binance-ws-api-userdata.test.ts --exit", "ws-live-tests": "mocha ./tests/binance-ws-spot.test.ts ./tests/binance-ws-futures.test.ts ./tests/binance-ws-api-userdata.test.ts ./tests/binance-ws-api-ticker.test.ts --exit", "test-debug": "mocha --inspect-brk", From 2e839e8abab00ca50001615f5b42dd72a02d3c59 Mon Sep 17 00:00:00 2001 From: carlosmiei <43336371+carlosmiei@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:44:19 +0100 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/node-binance-api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node-binance-api.ts b/src/node-binance-api.ts index ebf14ce2..123736a2 100644 --- a/src/node-binance-api.ts +++ b/src/node-binance-api.ts @@ -1868,8 +1868,9 @@ export default class Binance { // of silently dropping data — subscribe to each category on its own connection. const category = this.classifyFuturesStream(streams[0]); const mismatch = streams.find(s => this.classifyFuturesStream(s) !== category); - if (mismatch) { - throw new Error(`futuresSubscribe: cannot combine '${category}' stream "${streams[0]}" with '${this.classifyFuturesStream(mismatch)}' stream "${mismatch}" on one connection. Binance routes futures streams to separate /public, /market and /private endpoints; subscribe to each category separately.`); + if (mismatch !== undefined) { + const mismatchCategory = this.classifyFuturesStream(mismatch); + throw new Error(`futuresSubscribe: cannot combine '${category}' stream "${streams[0]}" with '${mismatchCategory}' stream "${mismatch}" on one connection. Binance routes futures streams to separate /public, /market and /private endpoints; subscribe to each category separately.`); } const baseUrl = this.getFStreamUrl(category); let ws: any = undefined;