CodeSampleX

샘플

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 and…

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

컨트랙트

  1. 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
  2. assert io() calls sharing the same origin reuse a single Manager instance and underlying transport connection unless forceNew is true
  3. assert disconnecting one multiplexed namespace leaves sibling namespaces and the underlying Manager transport connection open
  4. assert auth option is transmitted inside Socket.IO packet 40 payload rather than HTTP handshake query parameters or headers
  5. 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

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# 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`.
csx.json
{"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"}
package-lock.json
{
  "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"
      }
    }
  }
}
package.json
{
  "name": "sample",
  "version": "1.0.0",
  "type": "module",
  "license": "MIT-0",
  "dependencies": {
    "socket.io-client": "4.8.3"
  }
}
test/contract.mjs
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);
});

오리진 시더

csx-seed