0
votes

I am trying to add WebSocket functionality to an existing application which uses Spring WebFlux. It uses:

  • Spring boot 2.2.1.RELEASE
  • Tomcat container
  • Configured to serve jsp pages

When I try to connect to it through JavaScript (from inside a jsp page) I am receiving the error "failed: Error during WebSocket handshake: Unexpected response code: 404"

I noted that if I create a controller class mapping url '/ws/event-emitter' (the one mapped in my websocket configuration) accepting GET requests, it receives the requests from Javascript call when it tries to open the websocket connection, which made me more confused.

What am I missing?

After read some tutorials on the Internet I set my configuration as below:

<?xml version="1.0" encoding="UTF-8"?>

https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.1.RELEASE br.com.samsung monitor 0.0.1-SNAPSHOT monitor Health Check Monitor

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
     -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.projectreactor</groupId>
        <artifactId>reactor-spring</artifactId>
        <version>1.0.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.microsoft.sqlserver</groupId>
        <artifactId>mssql-jdbc</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- MongoDB -->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-mongodb</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-releasetrain</artifactId>
        <version>Lovelace-SR9</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

@Configuration

public class ReactiveWebSocketConfiguration {

@Autowired
private WebSocketHandler webSocketHandler;

@Bean
public HandlerMapping webSocketHandlerMapping() {
    Map<String, WebSocketHandler> map = new HashMap<>();
    map.put("/ws/event-emitter", webSocketHandler);

    SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
    handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
    handlerMapping.setUrlMap(map);
    return handlerMapping;
}

@Bean
public WebSocketHandlerAdapter handlerAdapter(WebSocketService webSocketService) {
    return new WebSocketHandlerAdapter(webSocketService);
}

@Bean
public WebSocketService webSocketService() {
    TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
    strategy.setMaxSessionIdleTimeout(0L);
    return new HandshakeWebSocketService(strategy);
}

}

@Component public class ReactiveWebSocketHandler implements WebSocketHandler {

private static final ObjectMapper json = new ObjectMapper();

private Flux<String> eventFlux = Flux.generate(sink -> {
    Event event = new Event(randomUUID().toString(), now().toString());
    try {
        sink.next(json.writeValueAsString(event));
    } catch (JsonProcessingException e) {
        sink.error(e);
    }
});

private Flux<String> intervalFlux = Flux.interval(Duration.ofMillis(1000L))
  .zipWith(eventFlux, (time, event) -> event);

@Override
public Mono<Void> handle(WebSocketSession webSocketSession) {
    return webSocketSession.send(intervalFlux
      .map(webSocketSession::textMessage))
      .and(webSocketSession.receive()
        .map(WebSocketMessage::getPayloadAsText).log());
}

}

		<script>
		    var clientWebSocket = new WebSocket("ws://localhost:6600/ws/event-emitter");
		    clientWebSocket.onopen = function() {
		        console.log("clientWebSocket.onopen", clientWebSocket);
		        console.log("clientWebSocket.readyState", "websocketstatus");
		        clientWebSocket.send("event-me-from-browser");
		    }
		    clientWebSocket.onclose = function(error) {
		        console.log("clientWebSocket.onclose", clientWebSocket, error);
		        events("Closing connection");
		    }
		    clientWebSocket.onerror = function(error) {
		        console.log("clientWebSocket.onerror", clientWebSocket, error);
		        events("An error occured");
		    }
		    clientWebSocket.onmessage = function(error) {
		        console.log("clientWebSocket.onmessage", clientWebSocket, error);
		        events(error.data);
		    }
		    function events(responseEvent) {
		        document.querySelector(".events").innerHTML += responseEvent + "<br>";
		    }
		</script>
2
Have you tried to set CORS configuration on your HandlerMapping?Felipe Bonfante
are the ports correct?Toerktumlare
@FelipeBonfante yes, I have already tried CORS configuration but no luck. Anyway my Javascript code is running from inside a jsp file the application itself is serving.Atílio Araújo
@ThomasAndolf yes, they are. I have changed the default 8080 port as I have another application using it.Atílio Araújo

2 Answers

3
votes

I could make it work removing spring-boot-starter-web and tomcat-embed-jasper dependencies. I suppose spring-boot-starter-web might conflict with spring-boot-starter-webflux.

Also I have changed my ReactiveWebSocketConfiguration class to set WebSocketService bean using ReactorNettyRequestUpgradeStrategy instead of TomcatRequestUpgradeStrategy:

@Bean
public WebSocketService webSocketService() {
    return new HandshakeWebSocketService(new ReactorNettyRequestUpgradeStrategy());
}

After those changes my jsp pages stopped working as my web container changed from Tomcat to Netty Web Server. I had to use Thymeleaf (added spring-boot-starter-thymeleaf dependency, configured its basic strucute and converted my pages to .html files) instead of traditional jsp.

I have opted to change from traditional jsp to Thymeleaf due to time constraints as my application has few and simple pages.

I have created a very simple project with all those configuration including a dummy page connecting to the websocket via javascript.

https://github.com/atilio-araujo/spring-webflux-websocket

2
votes

Spring WebFlux WebSocket:

Just adding this answer for people who are trying to achieve websocket with webflux.

  1. The OP has both web and webflux dependencies for some reason. However you just need WebFlux dependency only.
  2. Implement a WebSocketHandler
    @Service
    public class WebFluxWebSocketHandler implements WebSocketHandler {
    
        @Override
        public Mono<Void> handle(WebSocketSession webSocketSession) {
            Flux<WebSocketMessage> stringFlux = webSocketSession.receive()
                    .map(WebSocketMessage::getPayloadAsText)
                    .map(String::toUpperCase)
                    .map(webSocketSession::textMessage);
            return webSocketSession.send(stringFlux);
        }
    
    }
  1. Add a path - handler mapping. Here the handler is an instance of the websockethandler.
     @Bean
       public HandlerMapping handlerMapping(){
           Map<String, WebFluxWebSocketHandler> handlerMap = Map.of(
                   "/test", handler
           );
           return new SimpleUrlHandlerMapping(handlerMap, 1);
       }

That's it! Now the client should be able to connect to ws://localhost:8080/test

Check here for more info - https://www.vinsguru.com/spring-webflux-websocket/