Пример
socket.io-client 4.8.3: Resolve io() URL pathnames as namespaces rather than HTTP paths, multiplex distinct namespaces over a shared Manager and transport, isolate namespace disconnections, and transmit auth payloads within packet 40 rather than HTTP handshake headers
Проверенный пример — npm socket.io-client 4.8.3: Resolve io() URL pathnames as namespaces rather than HTTP paths, multiplex distinct namespaces over a shared…
sha256:cab9ce700b45a56b1027f2a67ee185a70fe74b723e406a4ca91485c3305fe447
Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла.
Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди.
MIT-0
Свидетельства выполнения
Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.
- Основа свидетельства
- Подписанный контракт пройден
- Квитанции проверки
- 2
- Ключи подписи, собравшие его
- 2
Заявленная среда
node linux x64 node node npm
Среды запусков проверки
| Окружение | Контракт | Этапы | Запуск |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-16 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
Кейс
HOW- Цель
- Resolve io() URL pathnames as namespaces rather than HTTP paths, multiplex distinct namespaces over a shared Manager and transport, isolate namespace disconnections, and transmit auth payloads within packet 40 rather than HTTP handshake headers
- Пакеты
- Символы
-
- io
- Manager
- Socket
- Окружение
- node
- Создан
- 2026-08-16T12:14:12Z
Контракт
- assert io() parses URL pathname as socket namespace and routes HTTP requests to /socket.io/ rather than URL path unless path option is explicitly set
- assert io() calls sharing the same origin reuse a single Manager instance and underlying transport connection unless forceNew is true
- assert disconnecting one multiplexed namespace leaves sibling namespaces and the underlying Manager transport connection open
- assert auth option is transmitted inside Socket.IO packet 40 payload rather than HTTP handshake query parameters or headers
- assert socket.emit returns the socket instance for chaining while socket.timeout on a disconnected socket begins immediately and rejects with operation has timed out
Файлы
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
Исходный код
# Socket.IO Client API Semantics and Connection Mechanics
## `search_known_solution` Result
`search_known_solution` returned `MATCH: COMPATIBLE` pointing to sample `sha256:a34e5524e2d70aa4a594e6e5c07de316ebbcb9ce2828e6283a63ad65c5913e95` (which proved why `socket.io-client` cannot connect to a plain `ws` WebSocket server due to Engine.IO handshakes and Socket.IO packet framing). That sample does not cover client-side `io()` URL parsing, Manager connection multiplexing, namespace lifecycle isolation, auth payload transmission, or offline timeout behaviors.
## What a naive model would write instead
A naive model assumes passing a pathname to `io("http://127.0.0.1:3000/admin/dashboard")` configures the HTTP/Engine.IO endpoint path (expecting HTTP requests to hit `/admin/dashboard` or `/admin/dashboard/socket.io`), that each `io()` call opens a separate transport connection, that calling `disconnect()` terminates the underlying connection, and that `auth` options are sent in HTTP request headers or query strings during handshake.
## How the wrong version fails
It fails silently with mismatched routing and unexpected connection reuse: HTTP reverse proxies return 404 because client requests target `/socket.io/` instead of the expected endpoint path unless `path` is set; sibling namespaces unexpectedly stay alive or leak events over the shared Manager transport when one namespace disconnects; and server-side HTTP handshake middleware never receives `auth` credentials because they are sent solely in Socket.IO CONNECT packet `40`.
{"case":{"believed":"Passing a URL pathname to io() sets the HTTP request path on the server and opens an independent transport connection per namespace, while auth options are sent in HTTP handshake headers or query parameters.","caseId":"case:sha256:8953a4ab60e55bd9f97b3fe7694918752b80d01093d61293cededeb73c138c33","contract":["assert io() parses URL pathname as socket namespace and routes HTTP requests to /socket.io/ rather than URL path unless path option is explicitly set","assert io() calls sharing the same origin reuse a single Manager instance and underlying transport connection unless forceNew is true","assert disconnecting one multiplexed namespace leaves sibling namespaces and the underlying Manager transport connection open","assert auth option is transmitted inside Socket.IO packet 40 payload rather than HTTP handshake query parameters or headers","assert socket.emit returns the socket instance for chaining while socket.timeout on a disconnected socket begins immediately and rejects with operation has timed out"],"goal":"Resolve io() URL pathnames as namespaces rather than HTTP paths, multiplex distinct namespaces over a shared Manager and transport, isolate namespace disconnections, and transmit auth payloads within packet 40 rather than HTTP handshake headers","kind":"HOW","packages":["pkg:npm/socket.io-client@4.8.3"],"schemaVersion":1,"symbols":["io","Manager","Socket"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/socket.io-client@4.8.3"],"schemaVersion":1,"symbols":["io","Manager","Socket"],"verifierAdapter":"node-typescript@1"}
{
"name": "sample",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sample",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"socket.io-client": "^4.8.3"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.21.0",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-client": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
"integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
}
}
}
{
"name": "sample",
"version": "1.0.0",
"type": "module",
"license": "MIT-0",
"dependencies": {
"socket.io-client": "4.8.3"
}
}
import assert from 'node:assert/strict';
import http from 'node:http';
import { io, Manager } from 'socket.io-client';
async function main() {
const httpRequests = [];
const postedPackets = [];
// Minimal HTTP server handling Engine.IO 4 + Socket.IO v4 protocol over HTTP polling
const server = http.createServer((req, res) => {
httpRequests.push({
url: req.url,
method: req.method,
headers: req.headers,
});
const parsed = new URL(req.url, 'http://127.0.0.1');
if (req.method === 'GET') {
if (!parsed.searchParams.has('sid')) {
// Step 1: Engine.IO polling handshake response (packet 0 OPEN)
res.writeHead(200, {
'Content-Type': 'text/plain; charset=UTF-8',
'Access-Control-Allow-Origin': '*',
});
res.end('0' + JSON.stringify({
sid: 'session-xyz-100',
upgrades: [],
pingInterval: 25000,
pingTimeout: 20000,
maxPayload: 1000000,
}));
} else {
// Step 2: Long-polling GET response acknowledging namespaces
res.writeHead(200, {
'Content-Type': 'text/plain; charset=UTF-8',
'Access-Control-Allow-Origin': '*',
});
// Respond with Socket.IO packet 40 (CONNECT ACK) for /chat and /admin namespaces
res.end('40/chat,{"sid":"sock-chat-01"}\x1e40/admin,{"sid":"sock-admin-01"}');
}
return;
}
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
postedPackets.push(body);
res.writeHead(200, {
'Content-Type': 'text/plain; charset=UTF-8',
'Access-Control-Allow-Origin': '*',
});
res.end('ok');
});
return;
}
res.writeHead(404).end();
});
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
// --- Contract 1: io(url) pathname becomes namespace (nsp), NOT HTTP path ---
{
const customUrlSocket = io(`http://127.0.0.1:${port}/admin/dashboard`, {
transports: ['polling'],
forceNew: true,
autoConnect: false,
});
assert.equal(customUrlSocket.nsp, '/admin/dashboard', 'URL pathname must be parsed as socket namespace');
assert.equal(customUrlSocket.io.opts.path, '/socket.io', 'HTTP request path defaults to /socket.io regardless of URL pathname');
const customPathSocket = io(`http://127.0.0.1:${port}/admin/dashboard`, {
path: '/custom-engine-path',
transports: ['polling'],
forceNew: true,
autoConnect: false,
});
assert.equal(customPathSocket.nsp, '/admin/dashboard', 'Namespace remains /admin/dashboard');
assert.equal(customPathSocket.io.opts.path, '/custom-engine-path', 'HTTP path is only overridden via explicit path option');
customUrlSocket.disconnect();
customPathSocket.disconnect();
}
// --- Contract 2: Multiplexing across namespaces on the same origin shares Manager & transport ---
const chatSocket = io(`http://127.0.0.1:${port}/chat`, {
transports: ['polling'],
query: { sharedHandshakeKey: 'handshake-val' },
auth: { token: 'secret-chat-token' },
});
const adminSocket = io(`http://127.0.0.1:${port}/admin`, {
transports: ['polling'],
auth: (cb) => cb({ token: 'dynamic-admin-token' }),
});
assert.equal(chatSocket.io, adminSocket.io, 'Multiplexed namespace sockets must share the exact same Manager instance');
assert.notEqual(chatSocket, adminSocket, 'Multiplexed namespaces return distinct Socket instances');
const forcedNewSocket = io(`http://127.0.0.1:${port}/chat`, {
transports: ['polling'],
forceNew: true,
autoConnect: false,
});
assert.notEqual(chatSocket.io, forcedNewSocket.io, 'forceNew must create a distinct Manager instance');
forcedNewSocket.disconnect();
// Wait for both multiplexed sockets to establish connection
await Promise.all([
new Promise((resolve) => chatSocket.on('connect', resolve)),
new Promise((resolve) => adminSocket.on('connect', resolve)),
]);
assert.equal(chatSocket.connected, true, 'Chat namespace socket connected');
assert.equal(adminSocket.connected, true, 'Admin namespace socket connected');
// Verify wire handshake: only ONE Engine.IO GET handshake request occurred
const handshakeRequests = httpRequests.filter((r) => r.method === 'GET' && !r.url.includes('sid='));
assert.equal(handshakeRequests.length, 1, 'Only one HTTP Engine.IO handshake occurs for shared Manager');
assert.ok(handshakeRequests[0].url.startsWith('/socket.io/?'), 'Handshake path is /socket.io/');
assert.ok(handshakeRequests[0].url.includes('sharedHandshakeKey=handshake-val'), 'Query option is in HTTP handshake URL');
assert.ok(!handshakeRequests[0].url.includes('secret-chat-token'), 'Auth payload is never exposed in HTTP query string');
assert.ok(!handshakeRequests[0].url.includes('dynamic-admin-token'), 'Dynamic auth payload is never in HTTP query string');
// Wait until both namespace connect POST packets are received
while (postedPackets.length < 2) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
// Verify auth payload is sent inside Socket.IO CONNECT packet 40
const joinedPackets = postedPackets.join(';;;');
assert.ok(joinedPackets.includes('40/chat,{"token":"secret-chat-token"}'), 'Auth payload is serialized inside packet 40/chat');
assert.ok(joinedPackets.includes('40/admin,{"token":"dynamic-admin-token"}'), 'Dynamic auth callback is serialized inside packet 40/admin');
// --- Contract 3: Namespace disconnect isolation ---
chatSocket.disconnect();
assert.equal(chatSocket.connected, false, 'chatSocket disconnected');
assert.equal(adminSocket.connected, true, 'adminSocket remains connected when sibling namespace disconnects');
assert.equal(chatSocket.io.engine.readyState, 'open', 'Underlying Engine.IO transport connection remains open');
// --- Contract 4: socket.emit chaining vs socket.timeout immediate timer ---
const emitResult = adminSocket.emit('testEvent', { payload: 'ok' });
assert.equal(emitResult, adminSocket, 'socket.emit must return the socket instance for chaining, not a Promise');
const offlineSocket = io(`http://127.0.0.1:${port}/offline`, {
transports: ['polling'],
forceNew: true,
autoConnect: false,
});
assert.equal(offlineSocket.connected, false);
const start = Date.now();
let timeoutError = null;
try {
await offlineSocket.timeout(50).emitWithAck('ping');
} catch (err) {
timeoutError = err;
}
const elapsed = Date.now() - start;
assert.ok(timeoutError instanceof Error, 'Offline emitWithAck timeout must reject with Error');
assert.equal(timeoutError.message, 'operation has timed out', 'Error message must be "operation has timed out"');
assert.ok(elapsed >= 40 && elapsed < 350, `Timeout timer must execute immediately without waiting for connection (elapsed: ${elapsed}ms)`);
// Cleanup: disconnect all active sockets and their managers
chatSocket.disconnect();
adminSocket.disconnect();
chatSocket.io.disconnect();
offlineSocket.disconnect();
offlineSocket.io.disconnect();
} finally {
server.close();
}
}
main().then(() => {
process.exit(0);
}).catch((err) => {
console.error('Contract failed:', err);
process.exit(1);
});