Hi guys I need some help with WebsocketSTOMP in React-Native Android app and Metro.
I can get the connection but no the STOMP handshake.
Frontend DevTool console:
Backend Terminal in Java SPring Boot:
nothing else no Stomp handshake. So the port is open and the connection is established.
Do someone know what is wrong or what settings are wrongly setup. On Expo it is working ok. I have no idea why not in Metro.
I created Backend:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .addInterceptors(new HttpHandshakeInterceptor()) .setAllowedOriginPatterns("*"); } @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new WebSocketEventInterceptor()); } } @Controller public class MessageController { @MessageMapping("/sendMessage") // Maps to /app/sendMessage @SendTo("/topic/messages") // Broadcasts to /topic/messages public Message sendMessage(Message message) { System.out.println("Received message: " + message); return message; } } public class Message { private String sender; private String content; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") private LocalDateTime timestamp; public Message() { this.timestamp = LocalDateTime.now(); } public Message(String sender, String content) { this(); this.sender = sender; this.content = content; } public String getSender() { return sender; } public void setSender(String sender) { this.sender = sender; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public LocalDateTime getTimestamp() { return timestamp; } public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } @Override public String toString() { return "Message{sender='" + sender + "', content='" + content + "', timestamp=" + timestamp + "}"; } } public class HttpHandshakeInterceptor implements HandshakeInterceptor { private static final Logger logger = LoggerFactory.getLogger(HttpHandshakeInterceptor.class); @Override public boolean beforeHandshake( ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request instanceof ServletServerHttpRequest servletRequest) { InetSocketAddress remoteAddr = servletRequest.getRemoteAddress(); String ip = (remoteAddr != null ? remoteAddr.getAddress().getHostAddress() : "unknown"); logger.info("WebSocket CONNECT attempt from IP={}", ip); // Store IP in session attributes so we can log it later in STOMP interceptor attributes.put("ip", ip); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { logger.info("WebSocket handshake complete."); } } @Configuration public class WebSocketEventInterceptor implements ChannelInterceptor { private static final Logger logger = LoggerFactory.getLogger(WebSocketEventInterceptor.class); @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null) { StompCommand command = accessor.getCommand(); if (StompCommand.CONNECT.equals(command)) { logger.info("STOMP CONNECT from sessionId={} user={} ip={}", accessor.getSessionId(), accessor.getUser(), accessor.getSessionAttributes() != null ? accessor.getSessionAttributes().get("ip") : "unknown"); } else if (StompCommand.SUBSCRIBE.equals(command)) { logger.info("STOMP SUBSCRIBE sessionId={} destination={}", accessor.getSessionId(), accessor.getDestination()); } else if (StompCommand.SEND.equals(command)) { logger.info("STOMP SEND sessionId={} destination={}", accessor.getSessionId(), accessor.getDestination()); } else if (StompCommand.DISCONNECT.equals(command)) { logger.info("STOMP DISCONNECT sessionId={}", accessor.getSessionId()); } } return message; } } And Frontend in React-Native for Android App runned via Metro on Emulated Pixel 6 on Android Studio:
import React, { useState, useEffect, useRef } from 'react'; import { View, Text, TextInput, Button, FlatList, StyleSheet, Alert, } from 'react-native'; import { Client } from '@stomp/stompjs'; import { EdgeInsets } from 'react-native-safe-area-context'; interface Message { sender: string; content: string; timestamp: string; } interface ChatScreenProps { safeAreaInsets: EdgeInsets; } const ChatScreen: React.FC<ChatScreenProps> = ({ safeAreaInsets }) => { const [client, setClient] = useState<Client | null>(null); const [messages, setMessages] = useState<Message[]>([]); const [inputText, setInputText] = useState<string>(''); const flatListRef = useRef<FlatList<Message>>(null); const isMounted = useRef<boolean>(true); const backendUrl = 'ws://10.0.2.2:8085'; useEffect(() => { if (!isMounted.current) return; console.log('Attempting to initialize WebSocket connection...'); const stompClient = new Client({ webSocketFactory: () => { console.log('Creating WebSocket connection to', `${backendUrl}/ws`); return new WebSocket(`${backendUrl}/ws`); }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000, debug: (str) => console.log('STOMP Debug:', str), }); stompClient.onConnect = (frame) => { console.log('WebSocket Connected:', frame); if (isMounted.current) { setClient(stompClient); stompClient.subscribe('/topic/messages', (message) => { if (isMounted.current) { console.log('Received message:', message.body); const msg: Message = JSON.parse(message.body); setMessages((prev) => [...prev, msg]); flatListRef.current?.scrollToEnd({ animated: true }); } }); } }; stompClient.onStompError = (frame) => { console.error('STOMP Error:', frame); if (isMounted.current) { Alert.alert('Connection Error', `STOMP failed: ${frame.headers['message'] || 'Unknown error'}`); } }; stompClient.onWebSocketError = (error) => { console.error('WebSocket Error:', error); if (isMounted.current) { Alert.alert('WebSocket Error', `Failed to connect: ${error.message || 'Unknown error'}`); } }; stompClient.onWebSocketClose = (event) => { console.log('WebSocket Closed:', event); if (isMounted.current) { Alert.alert('WebSocket Closed', `Connection closed: ${event.reason || 'Unknown reason'}`); } }; console.log('Activating STOMP client...'); stompClient.activate(); const timeout = setTimeout(() => { if (isMounted.current && !stompClient.connected) { console.error('WebSocket connection timed out after 10 seconds'); Alert.alert('Connection Timeout', 'Failed to connect to WebSocket server'); } }, 10000); return () => { console.log('Cleaning up WebSocket connection...'); isMounted.current = false; clearTimeout(timeout); if (stompClient) { stompClient.deactivate(); } }; }, []); const sendMessage = () => { if (client && client.connected && inputText.trim()) { console.log('Sending message:', inputText); client.publish({ destination: '/app/sendMessage', body: JSON.stringify({ sender: 'Frontend User', content: inputText, timestamp: new Date().toISOString(), }), }); setInputText(''); } else { Alert.alert('Error', 'Not connected or empty message'); } }; const renderMessage = ({ item }: { item: Message }) => ( <View style={styles.messageItem}> <Text style={styles.sender}>{item.sender}: </Text> <Text>{item.content}</Text> <Text style={styles.timestamp}>{new Date(item.timestamp).toLocaleTimeString()}</Text> </View> ); AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.INTERNET" /> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config"> <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> \android\app\src\main\res\xml\network_security_config.xml
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <!-- Allow cleartext traffic only for local dev server --> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">10.0.2.2</domain> </domain-config> </network-security-config> 
