You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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) ... 

问题原因

  1. Mock核心组件破坏WebSocket初始化:测试中Mock了SimpMessagingTemplate,该Bean是Spring WebSocket自动配置的核心依赖,Mock操作会导致WebSocket服务无法正常初始化,服务器无法处理升级请求,返回400错误。
  2. 测试逻辑与实际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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:02:06