Skip to content
Advertisement

Spring WebFlux and WebSocket

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"?>

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);
    }

}

and the Web socket handler class

@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());
    }

}

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>";
}

Advertisement

Answer

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

Advertisement