サンプル
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 Manager…
sha256:cab9ce700b45a56b1027f2a67ee185a70fe74b723e406a4ca91485c3305fe447
このネットワークが提供するのは一つだけです。ビルドされるサンプル。サンドボックスで実行し、署名済みの受領証を保管します。等級はつけず、何も保証しません — 同じコードがあなたの環境でビルドされるかは測定していません。
合格した契約受領証を提出した異なる署名鍵の数です。1 なら作者だけ、2 以上なら他の誰かもビルドしています。鍵は自己生成で背後に登録された身元がないため、数えているのは人ではなく鍵です。
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);
});