diff --git a/example/__tests__/nitrowebsockets.harness.ts b/example/__tests__/nitrowebsockets.harness.ts index a0ab8f7..b3718c6 100644 --- a/example/__tests__/nitrowebsockets.harness.ts +++ b/example/__tests__/nitrowebsockets.harness.ts @@ -305,6 +305,54 @@ describe('NitroWebSocket - Server-initiated close', () => { }); }); +describe('NitroWebSocket - Handshake headers', () => { + function handshake( + protocols?: string[], + extraHeaders?: Record + ): Promise<{ ws: NitroWebSocket; headers: Record }> { + return withTimeout( + new Promise<{ ws: NitroWebSocket; headers: Record }>( + (resolve, reject) => { + const ws = new NitroWebSocket( + `${WS_BASE}/ws/headers`, + protocols, + extraHeaders + ); + ws.onmessage = (e) => resolve({ ws, headers: JSON.parse(e.data) }); + ws.onerror = (err) => reject(new Error(`Connection error: ${err}`)); + } + ), + 5_000, + 'handshake header echo' + ); + } + + it('sends no Sec-WebSocket-Protocol and no Origin when no protocols requested', async () => { + const { ws, headers } = await handshake(); + expect(headers['sec-websocket-protocol']).toBe(undefined); + expect(headers.origin).toBe(undefined); + expect(ws.protocol).toBe(''); + await closeAndWait(ws); + }); + + it('offers requested subprotocols and reports the negotiated one', async () => { + const { ws, headers } = await handshake(['chat', 'superchat']); + expect(headers['sec-websocket-protocol']).toContain('chat'); + expect(headers['sec-websocket-protocol']).toContain('superchat'); + expect(ws.protocol).toBe('chat'); + await closeAndWait(ws); + }); + + it('still sends caller-supplied custom headers', async () => { + const { ws, headers } = await handshake(undefined, { + 'x-nitro-test': 'handshake', + }); + expect(headers['x-nitro-test']).toBe('handshake'); + expect(headers['sec-websocket-protocol']).toBe(undefined); + await closeAndWait(ws); + }); +}); + // ─── Error Handling ─────────────────────────────────────────────────────────── describe('NitroWebSocket - Error Handling', () => { diff --git a/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.cpp b/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.cpp index 48daf2f..0089082 100644 --- a/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.cpp +++ b/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.cpp @@ -65,6 +65,10 @@ int nitroWsCallback(lws* wsi, enum lws_callback_reasons reason, auto* conn = static_cast(lws_wsi_user(wsi)); switch (reason) { + case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: + if (conn) conn->handleFilterPreEstablish(wsi); + break; + case LWS_CALLBACK_CLIENT_ESTABLISHED: if (conn) conn->handleEstablished(wsi); break; @@ -139,6 +143,7 @@ void WebSocketConnection::connect( _url = url; _state = State::CONNECTING; + _negotiatedProtocol.clear(); #if defined(NITRO_WS_TRACING) ATrace_beginSection(("NitroWS connect " + url).c_str()); @@ -180,8 +185,8 @@ void WebSocketConnection::connect( i.port = port; i.path = path.c_str(); i.host = host.c_str(); - i.origin = host.c_str(); - i.protocol = protoStr.empty() ? "nitro-ws" : protoStr.c_str(); + i.protocol = protoStr.empty() ? nullptr : protoStr.c_str(); + i.local_protocol_name = "nitro-ws"; i.userdata = self.get(); i.ssl_connection = isWss ? LCCSCF_USE_SSL : 0; @@ -295,6 +300,14 @@ void WebSocketConnection::setOnError(OnError cb) { +// Server-selected subprotocol must be read here: lws detaches the header +// table before LWS_CALLBACK_CLIENT_ESTABLISHED fires. +void WebSocketConnection::handleFilterPreEstablish(lws* wsi) { + char buf[256]; + int n = lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_PROTOCOL); + if (n > 0) _negotiatedProtocol.assign(buf, static_cast(n)); +} + void WebSocketConnection::handleEstablished(lws* wsi) { #if defined(NITRO_WS_TRACING) ATrace_beginSection("NitroWS established"); @@ -303,9 +316,6 @@ void WebSocketConnection::handleEstablished(lws* wsi) { _state = State::OPEN; _redirectCount = 0; - const lws_protocols* proto = lws_get_protocol(wsi); - if (proto && proto->name) _negotiatedProtocol = proto->name; - if (_onOpen) { _onOpen(); } else { diff --git a/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.hpp b/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.hpp index 97be4a2..3491b51 100644 --- a/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.hpp +++ b/packages/react-native-nitro-websockets/android/src/main/cpp/WebSocketConnection.hpp @@ -45,6 +45,7 @@ class WebSocketConnection : public WebSocketConnectionBase { void setOnError(OnError cb) override; // lws callback handlers (internal, not part of the base interface) + void handleFilterPreEstablish(lws* wsi); void handleEstablished(lws* wsi); void handleReceive(const void* in, size_t len, bool isBinary); void handleReceiveFragment(lws* wsi, const void* in, size_t len); diff --git a/test-server/server.mjs b/test-server/server.mjs index 11fdac5..cdbcafd 100644 --- a/test-server/server.mjs +++ b/test-server/server.mjs @@ -227,6 +227,7 @@ const server = app.listen(PORT, '0.0.0.0', () => { // WebSocket endpoints for the nitrowebsockets harness. // /ws/echo -> echoes every frame back +// /ws/headers -> first message is the handshake headers as JSON // /ws/close?code=1011&reason=x&delay=200 -> server-initiated close handshake // /ws/kill?delay=200 -> socket destroyed, no close frame const wss = new WebSocketServer({ server }); @@ -236,7 +237,9 @@ wss.on('connection', (ws, req) => { ws.on('message', (data, isBinary) => ws.send(data, { binary: isBinary })); - if (url.pathname === '/ws/close') { + if (url.pathname === '/ws/headers') { + ws.send(JSON.stringify(req.headers)); + } else if (url.pathname === '/ws/close') { const code = Number(url.searchParams.get('code')) || 1011; const reason = url.searchParams.get('reason') ?? 'server shutdown'; setTimeout(() => ws.close(code, reason), delay);