Spring Boot WebSocket测试:WebSocket升级请求遭HTTP 400错误
WebSocket测试HTTP 400错误排查与修复
问题描述
测试Spring Boot应用的WebSocket端点时,HTTP升级WebSocket请求持续返回400错误,但生产环境中前端可正常连接WebSocket。相关代码与错误信息如下:
WebSocket控制器
package es.us.dp1.l3_04_24_25.pandemic.model.game; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.messaging.simp.SimpMessagingTemplate; import java.util.List; @Controller public class WebSocketGameController { private final GameService gameService; private final SimpMessagingTemplate messagingTemplate; @Autowired public WebSocketGameController(GameService gameService, SimpMessagingTemplate messagingTemplate) { this.gameService = gameService; this.messagingTemplate = messagingTemplate; } @MessageMapping("/lobbies") @SendTo("/topic/lobbies") public List<Game> findPublicLobbies() { return gameService.findPublicLobbies(); } @MessageMapping("/lobbies/find") public void findLobby(@Payload Integer gameId) { Game lobby = gameService.findPublicLobbyById(gameId); messagingTemplate.convertAndSend("/topic/lobby/" + gameId, lobby); } }
测试类(原错误版本)
package es.us.dp1.l3_04_24_25.pandemic.game; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import java.util.List; import java.util.concurrent.TimeUnit; import es.us.dp1.l3_04_24_25.pandemic.model.game.Game; import es.us.dp1.l3_04_24_25.pandemic.model.game.GameService; import es.us.dp1.l3_04_24_25.pandemic.model.game.WebSocketGameController; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.stomp.StompSession; import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.messaging.WebSocketStompClient; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebSocketGameControllerTest { @MockBean private GameService gameService; @MockBean private SimpMessagingTemplate messagingTemplate; private WebSocketStompClient stompClient; private StompSession stompSession; @LocalServerPort private int port; @BeforeEach void setUp() throws Exception { stompClient = new WebSocketStompClient(new StandardWebSocketClient()); stompSession = stompClient.connect("ws://localhost:" + port + "/ws", new StompSessionHandlerAdapter() {}).get(5, TimeUnit.SECONDS); } @Test void shouldFindPublicLobbies() throws Exception { Game game = new Game(); game.setId(1); game.setName("Test Game"); List<Game> games = List.of(game); when(gameService.findPublicLobbies()).thenReturn(games); stompSession.subscribe("/topic/lobbies", new StompSessionHandlerAdapter() {}); stompSession.send("/app/lobbies", null); ArgumentCaptor<List<Game>> captor = ArgumentCaptor.forClass(List.class); verify(messagingTemplate, times(1)).convertAndSend(eq("/topic/lobbies"), captor.capture()); List<Game> capturedGames = captor.getValue(); assertEquals(1, capturedGames.size()); assertEquals("Test Game", capturedGames.get(0).getName()); } }
错误信息
java.util.concurrent.ExecutionException: jakarta.websocket.DeploymentException: The HTTP response from the server [400] did not permit the HTTP upgrade to WebSocket at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396) at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2096) at org.springframework.util.concurrent.CompletableToListenableFutureAdapter.get(CompletableToListenableFutureAdapter.java:106) at es.us.dp1.l3_04_24_25.pandemic.game.WebSocketGameControllerTest.setUp(WebSocketGameControllerTest.java:50) Caused by: jakarta.websocket.DeploymentException: The HTTP response from the server [400] did not permit the HTTP upgrade to WebSocket at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServerRecursive(WsWebSocketContainer.java:384) at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServer(WsWebSocketContainer.java:179) ...
问题原因
- Mock核心组件破坏WebSocket初始化:测试中Mock了
SimpMessagingTemplate,该Bean是Spring WebSocket自动配置的核心依赖,Mock操作会导致WebSocket服务无法正常初始化,服务器无法处理升级请求,返回400错误。 - 测试逻辑与实际WebSocket流转冲突:
@SendTo注解会自动通过内置的SimpMessagingTemplate发送消息到指定topic,Mock该模板后,自动发送逻辑被阻断,同时测试试图验证Mock模板的调用,与实际消息流转逻辑矛盾。
修复方案
1. 移除不必要的Mock组件
删除测试类中对SimpMessagingTemplate的Mock,让Spring自动配置该核心组件:
// 移除这一行 @MockBean private SimpMessagingTemplate messagingTemplate;
2. 调整测试逻辑,使用真实客户端验证消息
通过Stomp客户端订阅目标topic,接收并验证服务器返回的消息,而非依赖Mock模板:
修改后的完整测试类:
package es.us.dp1.l3_04_24_25.pandemic.game; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import es.us.dp1.l3_04_24_25.pandemic.model.game.Game; import es.us.dp1.l3_04_24_25.pandemic.model.game.GameService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.messaging.simp.stomp.StompFrameHandler; import org.springframework.messaging.simp.stomp.StompHeaders; import org.springframework.messaging.simp.stomp.StompSession; import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.messaging.WebSocketStompClient; import org.springframework.web.socket.sockjs.client.SockJsClient; import org.springframework.web.socket.sockjs.client.Transport; import org.springframework.web.socket.sockjs.client.WebSocketTransport; import java.lang.reflect.Type; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebSocketGameControllerTest { @MockBean private GameService gameService; private WebSocketStompClient stompClient; private StompSession stompSession; @LocalServerPort private int port; @BeforeEach void setUp() throws Exception { // 兼容SockJS配置(若生产环境使用) Transport transport = new WebSocketTransport(new StandardWebSocketClient()); SockJsClient sockJsClient = new SockJsClient(List.of(transport)); stompClient = new WebSocketStompClient(sockJsClient); // 添加Jackson转换器,确保对象序列化/反序列化正常 stompClient.setMessageConverter(new org.springframework.messaging.converter.MappingJackson2MessageConverter()); stompSession = stompClient.connect("ws://localhost:" + port + "/ws", new StompSessionHandlerAdapter() {}) .get(5, TimeUnit.SECONDS); } @Test void shouldFindPublicLobbies() throws Exception { // 准备测试数据 Game game = new Game(); game.setId(1); game.setName("Test Game"); List<Game> games = List.of(game); when(gameService.findPublicLobbies()).thenReturn(games); // 阻塞队列用于异步接收消息 BlockingQueue<List<Game>> messageQueue = new LinkedBlockingQueue<>(); // 订阅/topic/lobbies并处理消息 stompSession.subscribe("/topic/lobbies", new StompFrameHandler() { @Override public Type getPayloadType(StompHeaders headers) { return new com.fasterxml.jackson.core.type.TypeReference<List<Game>>() {}.getType(); } @Override public void handleFrame(StompHeaders headers, Object payload) { messageQueue.add((List<Game>) payload); } }); // 发送请求到/app/lobbies stompSession.send("/app/lobbies", null); // 等待并获取消息 List<Game> receivedGames = messageQueue.poll(5, TimeUnit.SECONDS); // 验证结果 assertEquals(1, receivedGames.size()); assertEquals("Test Game", receivedGames.get(0).getName()); } }
3. 额外注意事项
- 若生产环境使用SockJS,测试时需用
SockJsClient连接,避免协议不兼容。 - 必须配置Jackson消息转换器,否则客户端无法正确解析
Game对象。 - 使用
BlockingQueue异步接收消息,确保能捕获服务器的响应。
内容的提问来源于stack exchange,提问作者Manuel
相关产品推荐
相关产品推荐

