This is an automated email from the ASF dual-hosted git repository.

funky-eyes pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 30f602eea3 optimize: removed the OkHttp3 dependency from the 
NamingServer. (#8077)
30f602eea3 is described below

commit 30f602eea37b5acf147c2b9903ffca50f0f6abeb
Author: funkye <[email protected]>
AuthorDate: Tue Apr 28 20:16:57 2026 +0800

    optimize: removed the OkHttp3 dependency from the NamingServer. (#8077)
---
 .gitignore                                         |   5 +
 changes/en-us/2.x.md                               |   2 +-
 changes/zh-cn/2.x.md                               |   2 +-
 console/pom.xml                                    |  10 +
 .../seata/console/config/RestClientConfig.java     |  38 +++
 .../mcp/service/impl/ConsoleRemoteServiceImpl.java |  37 ++-
 .../service/impl/ConsoleRemoteServiceImplTest.java | 166 ++++++++++
 namingserver/pom.xml                               |   4 -
 .../seata/namingserver/config/WebConfig.java       |  36 +--
 .../namingserver/filter/ConsoleRemotingFilter.java | 125 +++++---
 .../seata/namingserver/manager/NamingManager.java  |  54 ++--
 .../service/ConsoleLocalServiceImpl.java           |  33 +-
 .../seata/namingserver/NamingManagerTest.java      | 104 ++++---
 .../filter/ConsoleRemotingFilterTest.java          | 344 ++++++++++++++++-----
 .../service/ConsoleLocalServiceImplTest.java       | 140 +++++++++
 15 files changed, 877 insertions(+), 223 deletions(-)

diff --git a/.gitignore b/.gitignore
index 2eb9470ffe..f4ce63bf64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,6 +73,11 @@ dependency-reduced-pom.xml
 .DS_Store
 Thumbs.db
 *.orig
+.github/agents/**
+.github/instructions/**
+.github/prompts/**
+.github/skills/**
+.github/copilot-instructions.md
 
 #h2
 *.db
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 50415b22f4..fa0f8f5519 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -64,7 +64,7 @@ Add changes here for all PR submitted to the 2.x branch.
 - [[#8064](https://github.com/apache/incubator-seata/pull/8064)] bump 
org.apache.kafka:kafka-clients from 3.6.1 to 3.9.2
 - [[#8069](https://github.com/apache/incubator-seata/pull/8069)] bump 
org.assertj:assertj-core from 3.12.2 to 3.27.7
 - [[#8075](https://github.com/apache/incubator-seata/pull/8075)] bump 
at.yawk.lz4:lz4-java from 1.9.0 to 1.10.1
-
+- [[#8077](https://github.com/apache/incubator-seata/pull/8077)] removed the 
OkHttp3 dependency from the NamingServer
 
 ### security:
 
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 1f0eff234e..a939ad3293 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -65,7 +65,7 @@
 - [[#8064](https://github.com/apache/incubator-seata/pull/8064)] 将 
org.apache.kafka:kafka-clients 从 3.6.1 版本升级到 3.9.2 版本
 - [[#8069](https://github.com/apache/incubator-seata/pull/8069)] 将 
org.assertj:assertj-core 从 3.12.2 版本升级到 3.27.7
 - [[#8075](https://github.com/apache/incubator-seata/pull/8075)] 将 
at.yawk.lz4:lz4-java 从 1.9.0 版本升级到 1.10.1
-
+- [[#8077](https://github.com/apache/incubator-seata/pull/8077)] namingserver 
移除 okhttp3 依赖
 
 
 ### security:
diff --git a/console/pom.xml b/console/pom.xml
index 1fd057f32e..9cb1ffa640 100644
--- a/console/pom.xml
+++ b/console/pom.xml
@@ -134,6 +134,16 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
             <artifactId>jjwt-api</artifactId>
diff --git 
a/console/src/main/java/org/apache/seata/console/config/RestClientConfig.java 
b/console/src/main/java/org/apache/seata/console/config/RestClientConfig.java
new file mode 100644
index 0000000000..2804244755
--- /dev/null
+++ 
b/console/src/main/java/org/apache/seata/console/config/RestClientConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.console.config;
+
+import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.JdkClientHttpRequestFactory;
+import org.springframework.web.client.RestClient;
+
+import java.net.http.HttpClient;
+
+@Configuration(proxyBeanMethods = false)
+@SuppressWarnings("null")
+public class RestClientConfig {
+
+    @Bean("consoleRestClient")
+    @ConditionalOnMissingBean(name = "consoleLocalServiceImpl")
+    public RestClient consoleRestClient(RestClient.Builder restClientBuilder) {
+        HttpClient httpClient = HttpClient.newHttpClient();
+        JdkClientHttpRequestFactory requestFactory = new 
JdkClientHttpRequestFactory(httpClient);
+        return restClientBuilder.requestFactory(requestFactory).build();
+    }
+}
diff --git 
a/console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java
 
b/console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java
index 4dfb152b1c..8586acb3f5 100644
--- 
a/console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java
+++ 
b/console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java
@@ -27,6 +27,7 @@ import org.apache.seata.mcp.exception.ServiceCallException;
 import org.apache.seata.mcp.service.ConsoleApiService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
@@ -35,23 +36,28 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.RestClient;
 import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Objects;
 
 import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl;
 import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap;
 
 @ConditionalOnMissingBean(name = "consoleLocalServiceImpl")
 @Service
+@SuppressWarnings("null")
 public class ConsoleRemoteServiceImpl implements ConsoleApiService {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ConsoleRemoteServiceImpl.class);
 
     private final JwtTokenUtils jwtTokenUtils;
 
-    private final RestTemplate restTemplate;
+    private final RestClient restClient;
 
     private final ObjectMapper objectMapper;
 
@@ -59,11 +65,11 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
 
     public ConsoleRemoteServiceImpl(
             JwtTokenUtils jwtTokenUtils,
-            RestTemplate restTemplate,
+            @Qualifier("consoleRestClient") RestClient restClient,
             ObjectMapper objectMapper,
             NamingServerProperties namingServerProperties) {
         this.jwtTokenUtils = jwtTokenUtils;
-        this.restTemplate = restTemplate;
+        this.restClient = restClient;
         this.objectMapper = objectMapper;
         this.namingServerProperties = namingServerProperties;
         LOGGER.info("ConsoleRemoteServiceImpl initialized.");
@@ -103,7 +109,7 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
         HttpEntity<String> entity = new HttpEntity<>(headers);
         String responseBody;
         try {
-            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.GET, entity, String.class);
+            ResponseEntity<String> response = executeRequest(url, 
HttpMethod.GET, entity);
 
             responseBody = response.getBody();
 
@@ -143,7 +149,7 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
         HttpEntity<String> entity = new HttpEntity<>(headers);
         String responseBody;
         try {
-            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.GET, entity, String.class);
+            ResponseEntity<String> response = executeRequest(url, 
HttpMethod.GET, entity);
 
             responseBody = response.getBody();
 
@@ -183,7 +189,7 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
         HttpEntity<String> entity = new HttpEntity<>(headers);
         String responseBody;
         try {
-            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.DELETE, entity, String.class);
+            ResponseEntity<String> response = executeRequest(url, 
HttpMethod.DELETE, entity);
 
             responseBody = response.getBody();
 
@@ -223,7 +229,7 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
         HttpEntity<String> entity = new HttpEntity<>(headers);
         String responseBody;
         try {
-            ResponseEntity<String> response = restTemplate.exchange(url, 
HttpMethod.PUT, entity, String.class);
+            ResponseEntity<String> response = executeRequest(url, 
HttpMethod.PUT, entity);
 
             responseBody = response.getBody();
 
@@ -241,4 +247,19 @@ public class ConsoleRemoteServiceImpl implements 
ConsoleApiService {
             throw new ServiceCallException(errorMsg);
         }
     }
+
+    private ResponseEntity<String> executeRequest(String url, HttpMethod 
httpMethod, HttpEntity<String> entity)
+            throws RestClientException {
+        return restClient
+                .method(Objects.requireNonNull(httpMethod))
+                .uri(Objects.requireNonNull(url))
+                .headers(headers -> headers.addAll(entity.getHeaders()))
+                .exchange((request, response) -> new ResponseEntity<>(
+                        readResponseBody(response), response.getHeaders(), 
response.getStatusCode()));
+    }
+
+    private String 
readResponseBody(RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse 
response)
+            throws IOException {
+        return response.getBody() == null ? "" : 
StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
+    }
 }
diff --git 
a/console/src/test/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImplTest.java
 
b/console/src/test/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImplTest.java
new file mode 100644
index 0000000000..821dc8e418
--- /dev/null
+++ 
b/console/src/test/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImplTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.mcp.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.seata.console.config.WebSecurityConfig;
+import org.apache.seata.console.utils.JwtTokenUtils;
+import org.apache.seata.mcp.core.props.NameSpaceDetail;
+import org.apache.seata.mcp.core.props.NamingServerProperties;
+import org.apache.seata.mcp.exception.ServiceCallException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import 
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
+
+import java.util.HashMap;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.client.ExpectedCount.once;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.headerDoesNotExist;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+@SuppressWarnings("null")
+class ConsoleRemoteServiceImplTest {
+
+    private JwtTokenUtils jwtTokenUtils;
+
+    private MockRestServiceServer server;
+
+    private ConsoleRemoteServiceImpl consoleRemoteService;
+
+    @BeforeEach
+    void setUp() {
+        jwtTokenUtils = mock(JwtTokenUtils.class);
+        when(jwtTokenUtils.validateToken("jwt-token")).thenReturn(true);
+
+        NamingServerProperties namingServerProperties = new 
NamingServerProperties();
+        namingServerProperties.setProtocol("http");
+        namingServerProperties.setAddr(List.of("127.0.0.1:8081"));
+
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        server = MockRestServiceServer.bindTo(restClientBuilder).build();
+        RestClient restClient = restClientBuilder.build();
+        consoleRemoteService =
+                new ConsoleRemoteServiceImpl(jwtTokenUtils, restClient, new 
ObjectMapper(), namingServerProperties);
+
+        SecurityContextHolder.getContext()
+                .setAuthentication(new 
UsernamePasswordAuthenticationToken("user", "jwt-token", List.of()));
+    }
+
+    @AfterEach
+    void tearDown() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    void getCallTCShouldSendAuthorizationAndNamespaceHeaders() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setCluster("default");
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("x-custom", "value");
+        HashMap<String, String> queryParams = new HashMap<>();
+        queryParams.put("page", "1");
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:8081/api/v1/console/sessions?page=1";))
+                .andExpect(method(HttpMethod.GET))
+                .andExpect(header(WebSecurityConfig.AUTHORIZATION_HEADER, 
WebSecurityConfig.TOKEN_PREFIX + "jwt-token"))
+                .andExpect(header("x-seata-namespace", "public"))
+                .andExpect(header("x-seata-cluster", "default"))
+                .andExpect(header("x-custom", "value"))
+                .andRespond(withSuccess("remote-ok", 
org.springframework.http.MediaType.TEXT_PLAIN));
+
+        String response =
+                consoleRemoteService.getCallTC(nameSpaceDetail, 
"/api/v1/console/sessions", null, queryParams, headers);
+
+        server.verify();
+        assertEquals("remote-ok", response);
+    }
+
+    @Test
+    void putCallTCShouldEncodeVgroupAsQueryParameter() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setvGroup("vgroup-a");
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:8081/api/v1/console/sessions?vGroup=vgroup-a";))
+                .andExpect(method(HttpMethod.PUT))
+                .andExpect(header(WebSecurityConfig.AUTHORIZATION_HEADER, 
WebSecurityConfig.TOKEN_PREFIX + "jwt-token"))
+                .andExpect(header("x-seata-namespace", "public"))
+                .andExpect(headerDoesNotExist("x-seata-cluster"))
+                .andRespond(withSuccess("updated", 
org.springframework.http.MediaType.TEXT_PLAIN));
+
+        String response = consoleRemoteService.putCallTC(
+                nameSpaceDetail, "/api/v1/console/sessions", null, new 
HashMap<>(), new HttpHeaders());
+
+        server.verify();
+        assertEquals("updated", response);
+    }
+
+    @Test
+    void deleteCallTCShouldSendHeadersAndReturnResponse() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setCluster("default");
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("x-custom", "value");
+        HashMap<String, String> queryParams = new HashMap<>();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:8081/api/v1/console/sessions";))
+                .andExpect(method(HttpMethod.DELETE))
+                .andExpect(header(WebSecurityConfig.AUTHORIZATION_HEADER, 
WebSecurityConfig.TOKEN_PREFIX + "jwt-token"))
+                .andExpect(header("x-seata-namespace", "public"))
+                .andExpect(header("x-seata-cluster", "default"))
+                .andExpect(header("x-custom", "value"))
+                .andRespond(withSuccess("deleted-ok", 
org.springframework.http.MediaType.TEXT_PLAIN));
+
+        String response = consoleRemoteService.deleteCallTC(
+                nameSpaceDetail, "/api/v1/console/sessions", null, 
queryParams, headers);
+
+        server.verify();
+        assertEquals("deleted-ok", response);
+    }
+
+    @Test
+    void getCallNameSpaceShouldThrowWhenUpstreamReturnsNon2xx() {
+        server.expect(once(), 
requestTo("http://127.0.0.1:8081/api/v1/console/namespaces";))
+                .andExpect(method(HttpMethod.GET))
+                .andExpect(header(WebSecurityConfig.AUTHORIZATION_HEADER, 
WebSecurityConfig.TOKEN_PREFIX + "jwt-token"))
+                
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR).body("boom"));
+
+        assertThrows(
+                ServiceCallException.class, () -> 
consoleRemoteService.getCallNameSpace("/api/v1/console/namespaces"));
+        server.verify();
+    }
+}
diff --git a/namingserver/pom.xml b/namingserver/pom.xml
index 03bfb3c5a6..2314aa16bd 100644
--- a/namingserver/pom.xml
+++ b/namingserver/pom.xml
@@ -169,10 +169,6 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
         </dependency>
-        <dependency>
-            <groupId>com.squareup.okhttp3</groupId>
-            <artifactId>okhttp</artifactId>
-        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/config/WebConfig.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/config/WebConfig.java
index f7d0a79f08..78afca5151 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/config/WebConfig.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/config/WebConfig.java
@@ -17,21 +17,18 @@
 package org.apache.seata.namingserver.config;
 
 import jakarta.servlet.Filter;
-import okhttp3.Dispatcher;
-import okhttp3.OkHttpClient;
 import org.apache.seata.namingserver.filter.ConsoleRemotingFilter;
 import org.apache.seata.namingserver.manager.NamingManager;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.Ordered;
-import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
-import org.springframework.web.client.RestTemplate;
+import org.springframework.http.client.JdkClientHttpRequestFactory;
+import org.springframework.web.client.RestClient;
 
-import java.util.concurrent.TimeUnit;
+import java.net.http.HttpClient;
+import java.time.Duration;
 
-import static 
org.apache.seata.namingserver.contants.NamingConstant.DEFAULT_CONNECTION_MAX_PER_ROUTE;
-import static 
org.apache.seata.namingserver.contants.NamingConstant.DEFAULT_CONNECTION_MAX_TOTAL;
 import static 
org.apache.seata.namingserver.contants.NamingConstant.DEFAULT_REQUEST_TIMEOUT;
 import static 
org.apache.seata.namingserver.contants.NamingConstant.DEFAULT_WRITE_TIMEOUT;
 
@@ -39,26 +36,19 @@ import static 
org.apache.seata.namingserver.contants.NamingConstant.DEFAULT_WRIT
 public class WebConfig {
 
     @Bean
-    public RestTemplate restTemplate() {
-        Dispatcher dispatcher = new Dispatcher();
-        dispatcher.setMaxRequests(DEFAULT_CONNECTION_MAX_TOTAL);
-        dispatcher.setMaxRequestsPerHost(DEFAULT_CONNECTION_MAX_PER_ROUTE);
-
-        OkHttpClient client = new OkHttpClient.Builder()
-                .dispatcher(dispatcher)
-                .connectTimeout(DEFAULT_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
-                .readTimeout(DEFAULT_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
-                .writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.MILLISECONDS)
+    public RestClient restClient(RestClient.Builder restClientBuilder) {
+        HttpClient httpClient = HttpClient.newBuilder()
+                .connectTimeout(Duration.ofMillis(DEFAULT_REQUEST_TIMEOUT))
+                .version(HttpClient.Version.HTTP_1_1)
                 .build();
-
-        // Create and return a RestTemplate with the custom request factory
-        return new RestTemplate(new OkHttp3ClientHttpRequestFactory(client));
+        JdkClientHttpRequestFactory requestFactory = new 
JdkClientHttpRequestFactory(httpClient);
+        
requestFactory.setReadTimeout(Duration.ofMillis(Math.max(DEFAULT_REQUEST_TIMEOUT,
 DEFAULT_WRITE_TIMEOUT)));
+        return restClientBuilder.requestFactory(requestFactory).build();
     }
 
     @Bean
-    public FilterRegistrationBean<Filter> consoleRemotingFilter(
-            NamingManager namingManager, RestTemplate restTemplate) {
-        ConsoleRemotingFilter consoleRemotingFilter = new 
ConsoleRemotingFilter(namingManager, restTemplate);
+    public FilterRegistrationBean<Filter> consoleRemotingFilter(NamingManager 
namingManager, RestClient restClient) {
+        ConsoleRemotingFilter consoleRemotingFilter = new 
ConsoleRemotingFilter(namingManager, restClient);
         FilterRegistrationBean<Filter> registration = new 
FilterRegistrationBean<>();
         registration.setFilter(consoleRemotingFilter);
         registration.addUrlPatterns("/*");
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilter.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilter.java
index b4c85a62fc..56a41c23b1 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilter.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilter.java
@@ -36,14 +36,17 @@ import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestTemplate;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.RestClient;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.regex.Pattern;
 
@@ -54,15 +57,15 @@ public class ConsoleRemotingFilter implements Filter {
 
     private final NamingManager namingManager;
 
-    private final RestTemplate restTemplate;
+    private final RestClient restClient;
 
     private final Pattern urlPattern = Pattern.compile(CONSOLE_PATTERN);
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ConsoleRemotingFilter.class);
 
-    public ConsoleRemotingFilter(NamingManager namingManager, RestTemplate 
restTemplate) {
+    public ConsoleRemotingFilter(NamingManager namingManager, RestClient 
restClient) {
         this.namingManager = namingManager;
-        this.restTemplate = restTemplate;
+        this.restClient = restClient;
     }
 
     /**
@@ -96,24 +99,25 @@ public class ConsoleRemotingFilter implements Filter {
         }
         int i = 0;
         // Skip optional UTF-8 BOM (0xEF, 0xBB, 0xBF)
-        if (body.length >= 3
-                && (body[0] & 0xFF) == 0xEF
-                && (body[1] & 0xFF) == 0xBB
-                && (body[2] & 0xFF) == 0xBF) {
+        if (body.length >= 3 && (body[0] & 0xFF) == 0xEF && (body[1] & 0xFF) 
== 0xBB && (body[2] & 0xFF) == 0xBF) {
             i = 3;
         }
         // skip leading whitespace (including Unicode NBSP / BOM that survived 
as whitespace)
-        while (i < body.length && (body[i] == ' ' || body[i] == '\t'
-                || body[i] == '\r' || body[i] == '\n')) {
+        while (i < body.length && (body[i] == ' ' || body[i] == '\t' || 
body[i] == '\r' || body[i] == '\n')) {
             i++;
         }
         if (i >= body.length) {
             return true;
         }
         byte first = body[i];
-        return first == '{' || first == '[' || first == '"'
-                || first == 't' || first == 'f' || first == 'n'
-                || (first >= '0' && first <= '9') || first == '-';
+        return first == '{'
+                || first == '['
+                || first == '"'
+                || first == 't'
+                || first == 'f'
+                || first == 'n'
+                || (first >= '0' && first <= '9')
+                || first == '-';
     }
 
     @Override
@@ -131,7 +135,7 @@ public class ConsoleRemotingFilter implements Filter {
                 String vgroup = request.getParameter("vgroup");
                 if (StringUtils.isNotBlank(namespace)
                         && (StringUtils.isNotBlank(cluster) || 
StringUtils.isNotBlank(vgroup))) {
-                    List<NamingServerNode> list = null;
+                    List<NamingServerNode> list = Collections.emptyList();
                     if (StringUtils.isNotBlank(vgroup)) {
                         list = namingManager.getInstancesByVgroupAndNamespace(
                                 namespace,
@@ -158,24 +162,29 @@ public class ConsoleRemotingFilter implements Filter {
                             if (node.getRole() == ClusterRole.LEADER) {
                                 headers.add(RAFT_GROUP_HEADER, node.getUnit());
                             }
-                            Collections.list(request.getHeaderNames())
-                                    .forEach(headerName -> {
-                                        if 
(!HttpHeaders.HOST.equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.CONNECTION.equalsIgnoreCase(headerName)
-                                                && 
!"Keep-Alive".equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.PROXY_AUTHENTICATE.equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.PROXY_AUTHORIZATION.equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.TE.equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.TRAILER.equalsIgnoreCase(headerName)
-                                                && 
!HttpHeaders.UPGRADE.equalsIgnoreCase(headerName)) {
-                                            headers.add(headerName, 
request.getHeader(headerName));
-                                        }
-                                    });
+                            
Collections.list(request.getHeaderNames()).forEach(headerName -> {
+                                if 
(!HttpHeaders.HOST.equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.CONNECTION.equalsIgnoreCase(headerName)
+                                        && 
!"Keep-Alive".equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.PROXY_AUTHENTICATE.equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.PROXY_AUTHORIZATION.equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.TE.equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.TRAILER.equalsIgnoreCase(headerName)
+                                        && 
!HttpHeaders.UPGRADE.equalsIgnoreCase(headerName)) {
+                                    String headerValue = 
request.getHeader(headerName);
+                                    // headerName comes from the Servlet API 
Enumeration, which JDT treats as @Nullable.
+                                    // The Servlet specification guarantees 
header names are never null;
+                                    // this defensive null guard is added 
solely to suppress the type-safety warning.
+                                    if (headerName != null && headerValue != 
null) {
+                                        headers.add(headerName, headerValue);
+                                    }
+                                }
+                            });
 
                             // Create the HttpEntity with headers and body
                             HttpMethod httpMethod;
                             try {
-                                httpMethod = 
HttpMethod.valueOf(request.getMethod());
+                                httpMethod = 
HttpMethod.valueOf(Objects.requireNonNull(request.getMethod()));
                             } catch (IllegalArgumentException ex) {
                                 LOGGER.error("Unsupported HTTP method: {}", 
request.getMethod(), ex);
                                 
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
@@ -197,7 +206,8 @@ public class ConsoleRemotingFilter implements Filter {
                                     // headers-only for empty body
                                     httpEntity = new HttpEntity<>(headers);
                                 } else {
-                                    // Remove potentially stale 
length/transfer headers and let the client recompute them
+                                    // Remove potentially stale 
length/transfer headers and let the client recompute
+                                    // them
                                     headers.remove(HttpHeaders.CONTENT_LENGTH);
                                     
headers.remove(HttpHeaders.TRANSFER_ENCODING);
                                     httpEntity = new HttpEntity<>(body, 
headers);
@@ -205,8 +215,10 @@ public class ConsoleRemotingFilter implements Filter {
                             }
 
                             try {
-                                ResponseEntity<byte[]> responseEntity = 
restTemplate.exchange(URI.create(targetUrl), httpMethod, httpEntity, 
byte[].class);
-                                //Copy headers from proxied response, skipping 
hop-by-hop and headers we manage ourselves to mitigate
+                                ResponseEntity<byte[]> responseEntity =
+                                        
executeProxyRequest(URI.create(targetUrl), httpMethod, httpEntity);
+                                // Copy headers from proxied response, 
skipping hop-by-hop and headers we manage
+                                // ourselves to mitigate
                                 // security risks from Content-Type 
manipulation
                                 responseEntity.getHeaders().forEach((key, 
value) -> {
                                     if 
(!HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(key)
@@ -225,25 +237,30 @@ public class ConsoleRemotingFilter implements Filter {
                                 });
                                 // Force a safe Content-Type: reject HTML/XML 
types that could
                                 // execute scripts; fall back to 
application/json
-                                String proxiedContentType = 
responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
+                                String proxiedContentType =
+                                        
responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
                                 String safeContentType;
-                                if (isSafeContentType(proxiedContentType)) {
+                                if (proxiedContentType != null && 
isSafeContentType(proxiedContentType)) {
                                     safeContentType = proxiedContentType;
                                 } else {
                                     safeContentType = 
"application/json;charset=UTF-8";
                                 }
                                 response.setContentType(safeContentType);
                                 response.setHeader("X-Content-Type-Options", 
"nosniff");
-                                
response.setStatus(responseEntity.getStatusCode().value());
+                                response.setStatus(
+                                        
responseEntity.getStatusCode().value());
                                 byte[] responseBody = responseEntity.getBody();
                                 // HEAD responses must not include a message 
body (RFC 7231 §4.3.2)
                                 if (!HttpMethod.HEAD.equals(httpMethod)
-                                        && responseBody != null && 
responseBody.length > 0) {
+                                        && responseBody != null
+                                        && responseBody.length > 0) {
                                     // For JSON content type, validate that 
the body actually looks
                                     // like JSON to prevent XSS via crafted 
upstream responses
                                     if 
(safeContentType.toLowerCase(Locale.ROOT).contains("application/json")
                                             && !looksLikeJson(responseBody)) {
-                                        LOGGER.warn("Upstream returned 
non-JSON body for Content-Type {}, replacing with error response", 
safeContentType);
+                                        LOGGER.warn(
+                                                "Upstream returned non-JSON 
body for Content-Type {}, replacing with error response",
+                                                safeContentType);
                                         
response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
                                         
response.setContentType("application/json;charset=UTF-8");
                                         responseBody = "{\"error\":\"Upstream 
returned invalid response body\"}"
@@ -256,7 +273,10 @@ public class ConsoleRemotingFilter implements Filter {
                                         // Client likely disconnected (broken 
pipe); log at debug
                                         // level and do NOT attempt sendError 
– the response may
                                         // already be committed.
-                                        LOGGER.debug("Failed to write proxy 
response body (client disconnect?): {}", e.getMessage(), e);
+                                        LOGGER.debug(
+                                                "Failed to write proxy 
response body (client disconnect?): {}",
+                                                e.getMessage(),
+                                                e);
                                     }
                                 }
                             } catch (Exception ex) {
@@ -271,4 +291,35 @@ public class ConsoleRemotingFilter implements Filter {
         }
         filterChain.doFilter(servletRequest, servletResponse);
     }
+
+    private ResponseEntity<byte[]> executeProxyRequest(
+            URI targetUrl, HttpMethod httpMethod, HttpEntity<byte[]> 
httpEntity) {
+        RestClient.RequestBodySpec requestSpec = restClient
+                .method(Objects.requireNonNull(httpMethod))
+                .uri(Objects.requireNonNull(targetUrl))
+                .headers(headers -> headers.addAll(httpEntity.getHeaders()));
+        RestClient.RequestHeadersSpec<?> exchangeSpec = requestSpec;
+        byte[] requestBody = httpEntity.getBody();
+        if (requestBody != null
+                && requestBody.length > 0
+                && !HttpMethod.GET.equals(httpMethod)
+                && !HttpMethod.HEAD.equals(httpMethod)) {
+            exchangeSpec = requestSpec.body(requestBody);
+        }
+        return exchangeSpec.exchange((req, response) -> {
+            byte[] bodyBytes;
+            if (HttpMethod.HEAD.equals(httpMethod)) {
+                // HEAD responses must not contain a message body per RFC 7231 
§4.3.2.
+                bodyBytes = null;
+            } else {
+                // For 204/304 and similar no-body responses, some HTTP client 
implementations
+                // (e.g. JDK HTTP Client) may return null from getBody().
+                // Treat a null stream as an empty byte array defensively,
+                // consistent with the null guards in ConsoleLocalServiceImpl 
and ConsoleRemoteServiceImpl.
+                InputStream bodyStream = response.getBody();
+                bodyBytes = (bodyStream != null) ? 
StreamUtils.copyToByteArray(bodyStream) : new byte[0];
+            }
+            return new ResponseEntity<>(bodyBytes, response.getHeaders(), 
response.getStatusCode());
+        });
+    }
 }
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
index a2f13a79d8..263e26bb87 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/manager/NamingManager.java
@@ -21,7 +21,6 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
 import com.github.benmanes.caffeine.cache.RemovalCause;
 import com.github.benmanes.caffeine.cache.RemovalListener;
 import jakarta.annotation.PostConstruct;
-import okhttp3.Response;
 import org.apache.seata.common.NamingServerConstants;
 import org.apache.seata.common.metadata.Cluster;
 import org.apache.seata.common.metadata.ClusterRole;
@@ -30,7 +29,6 @@ import 
org.apache.seata.common.metadata.namingserver.NamingServerNode;
 import org.apache.seata.common.metadata.namingserver.Unit;
 import org.apache.seata.common.result.Result;
 import org.apache.seata.common.result.SingleResult;
-import org.apache.seata.common.util.HttpClientUtil;
 import org.apache.seata.common.util.StringUtils;
 import org.apache.seata.namingserver.entity.bo.ClusterBO;
 import org.apache.seata.namingserver.entity.bo.NamespaceBO;
@@ -50,15 +48,19 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.util.UriComponentsBuilder;
 
-import java.io.IOException;
 import java.net.InetSocketAddress;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -94,6 +96,9 @@ public class NamingManager {
     @Autowired
     private NamingServerMetricsManager metricsManager;
 
+    @Autowired
+    private RestClient restClient;
+
     public NamingManager() {
         this.instanceLiveTable = new ConcurrentHashMap<>();
         this.namespaceClusterDataMap = new ConcurrentHashMap<>();
@@ -221,21 +226,17 @@ public class NamingManager {
             HashMap<String, String> params = new HashMap<>();
             params.put(CONSTANT_GROUP, vGroup);
             params.put(NamingServerConstants.CONSTANT_UNIT, actualUnitName);
-            Map<String, String> header = new HashMap<>();
-            header.put("Content-Type", "application/x-www-form-urlencoded");
-
-            try (Response httpResponse = HttpClientUtil.doGet(httpUrl, params, 
header, 3000)) {
-                if (httpResponse == null || httpResponse.code() != 200) {
-                    return new Result<>(
-                            String.valueOf(httpResponse != null ? 
httpResponse.code() : 500),
-                            "add vGroup in new cluster failed");
+            try {
+                int statusCode = executeControlRequest(httpUrl, params);
+                if (statusCode != 200) {
+                    return new Result<>(String.valueOf(statusCode), "add 
vGroup in new cluster failed");
                 }
                 LOGGER.info(
                         "namespace: {} add vGroup: {} in new cluster: {} 
successfully!",
                         namespace,
                         vGroup,
                         clusterName);
-            } catch (IOException e) {
+            } catch (RestClientException e) {
                 LOGGER.error("add vGroup in new cluster failed:{}", 
e.getMessage(), e);
                 return new Result<>("500", "add vGroup in new cluster failed");
             }
@@ -254,13 +255,12 @@ public class NamingManager {
             HashMap<String, String> params = new HashMap<>();
             params.put(CONSTANT_GROUP, vGroup);
             params.put(NamingServerConstants.CONSTANT_UNIT, unitName);
-            Map<String, String> header = new HashMap<>();
-            header.put("Content-Type", "application/x-www-form-urlencoded");
-            try (Response httpResponse = HttpClientUtil.doGet(httpUrl, params, 
header, 3000)) {
-                if (httpResponse == null || httpResponse.code() != 200) {
+            try {
+                int statusCode = executeControlRequest(httpUrl, params);
+                if (statusCode != 200) {
                     LOGGER.warn("remove vGroup in old cluster failed");
                     return new Result<>(
-                            String.valueOf(httpResponse != null ? 
httpResponse.code() : 500),
+                            String.valueOf(statusCode),
                             "removing vGroup " + vGroup + " in old cluster " + 
clusterName + " failed");
                 }
                 LOGGER.info(
@@ -268,7 +268,7 @@ public class NamingManager {
                         namespace,
                         vGroup,
                         clusterName);
-            } catch (IOException e) {
+            } catch (RestClientException e) {
                 LOGGER.error("handle removing vGroup in old cluster 
failed:{}", e.getMessage(), e);
                 return new Result<>(
                         "500", "handle removing vGroup " + vGroup + " in old 
cluster " + clusterName + " failed");
@@ -277,6 +277,24 @@ public class NamingManager {
         return new Result<>("200", "remove group in old cluster 
successfully!");
     }
 
+    private int executeControlRequest(String httpUrl, Map<String, String> 
params) {
+        URI targetUri = buildControlUri(httpUrl, params);
+        Integer statusCode = restClient
+                .get()
+                .uri(targetUri)
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .exchange((request, response) -> 
response.getStatusCode().value());
+        return Objects.requireNonNull(statusCode);
+    }
+
+    private URI buildControlUri(String httpUrl, Map<String, String> params) {
+        String rawUrl = Objects.requireNonNull(httpUrl);
+        String baseUrl = rawUrl.endsWith("?") ? rawUrl.substring(0, 
rawUrl.length() - 1) : rawUrl;
+        UriComponentsBuilder builder = 
UriComponentsBuilder.fromUriString(baseUrl);
+        params.forEach(builder::queryParam);
+        return Objects.requireNonNull(builder.build().encode().toUri());
+    }
+
     public boolean addGroup(String namespace, String clusterName, String 
unitName, String vGroup) {
         try {
             ClusterBO clusterBO = vGroupMap
diff --git 
a/namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java
 
b/namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java
index 782e2c3019..3dcbc94331 100644
--- 
a/namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java
+++ 
b/namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java
@@ -36,11 +36,16 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.RestClient;
 import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ThreadLocalRandom;
 
 import static org.apache.seata.common.Constants.RAFT_GROUP_HEADER;
@@ -56,13 +61,13 @@ public class ConsoleLocalServiceImpl implements 
ConsoleApiService {
 
     private final NamingManager namingManager;
 
-    private final RestTemplate restTemplate;
+    private final RestClient restClient;
 
     private final ObjectMapper objectMapper;
 
-    public ConsoleLocalServiceImpl(NamingManager namingManager, RestTemplate 
restTemplate, ObjectMapper objectMapper) {
+    public ConsoleLocalServiceImpl(NamingManager namingManager, RestClient 
restClient, ObjectMapper objectMapper) {
         this.namingManager = namingManager;
-        this.restTemplate = restTemplate;
+        this.restClient = restClient;
         this.objectMapper = objectMapper;
         LOGGER.info("ConsoleLocalServiceImpl initialized.");
     }
@@ -78,7 +83,7 @@ public class ConsoleLocalServiceImpl implements 
ConsoleApiService {
         String cluster = nameSpaceDetail.getCluster();
         String vgroup = nameSpaceDetail.getvGroup();
         if (StringUtils.isNotBlank(namespace) && 
(StringUtils.isNotBlank(cluster) || StringUtils.isNotBlank(vgroup))) {
-            List<NamingServerNode> list = null;
+            List<NamingServerNode> list = Collections.emptyList();
             if (StringUtils.isNotBlank(vgroup)) {
                 list = namingManager.getInstancesByVgroupAndNamespace(
                         namespace, vgroup, HttpMethod.GET.equals(httpMethod));
@@ -100,8 +105,7 @@ public class ConsoleLocalServiceImpl implements 
ConsoleApiService {
                     HttpEntity<String> entity = new HttpEntity<>(headers);
                     String responseBody;
                     try {
-                        ResponseEntity<String> response =
-                                restTemplate.exchange(targetUrl, httpMethod, 
entity, String.class);
+                        ResponseEntity<String> response = 
executeRequest(targetUrl, httpMethod, entity);
 
                         responseBody = response.getBody();
 
@@ -125,6 +129,21 @@ public class ConsoleLocalServiceImpl implements 
ConsoleApiService {
         throw new IllegalArgumentException("Invalid NameSpace Detail");
     }
 
+    private ResponseEntity<String> executeRequest(String targetUrl, HttpMethod 
httpMethod, HttpEntity<String> entity)
+            throws RestClientException {
+        return restClient
+                .method(Objects.requireNonNull(httpMethod))
+                .uri(Objects.requireNonNull(targetUrl))
+                .headers(headers -> headers.addAll(entity.getHeaders()))
+                .exchange((request, response) -> new ResponseEntity<>(
+                        readResponseBody(response), response.getHeaders(), 
response.getStatusCode()));
+    }
+
+    private String 
readResponseBody(RestClient.RequestHeadersSpec.ConvertibleClientHttpResponse 
response)
+            throws IOException {
+        return response.getBody() == null ? "" : 
StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
+    }
+
     @Override
     public String getCallTC(
             NameSpaceDetail nameSpaceDetail,
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
index 36eaccca81..074adf80a5 100644
--- 
a/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/NamingManagerTest.java
@@ -16,8 +16,7 @@
  */
 package org.apache.seata.namingserver;
 
-import okhttp3.Response;
-import okhttp3.ResponseBody;
+import org.apache.seata.common.NamingServerConstants;
 import org.apache.seata.common.metadata.Cluster;
 import org.apache.seata.common.metadata.ClusterRole;
 import org.apache.seata.common.metadata.Node;
@@ -25,7 +24,6 @@ import 
org.apache.seata.common.metadata.namingserver.NamingServerNode;
 import org.apache.seata.common.metadata.namingserver.Unit;
 import org.apache.seata.common.result.Result;
 import org.apache.seata.common.result.SingleResult;
-import org.apache.seata.common.util.HttpClientUtil;
 import org.apache.seata.namingserver.entity.pojo.ClusterData;
 import org.apache.seata.namingserver.entity.vo.monitor.ClusterVO;
 import org.apache.seata.namingserver.entity.vo.v2.NamespaceVO;
@@ -36,14 +34,17 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
 import org.mockito.quality.Strictness;
 import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
 import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -53,11 +54,14 @@ import java.util.Map;
 import java.util.UUID;
 
 import static org.apache.seata.common.NamingServerConstants.CONSTANT_GROUP;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
+import static org.springframework.test.web.client.ExpectedCount.once;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
 
 @ExtendWith(MockitoExtension.class)
 @MockitoSettings(strictness = Strictness.LENIENT)
@@ -65,31 +69,22 @@ class NamingManagerTest {
 
     private NamingManager namingManager;
 
-    @Mock
     private ApplicationContext applicationContext;
 
-    @Mock
-    private Response httpResponse;
-
-    @Mock
-    private ResponseBody responseBody;
-
-    private MockedStatic<HttpClientUtil> mockedHttpClientUtil;
+    private MockRestServiceServer server;
 
     @BeforeEach
     void setUp() {
         namingManager = new NamingManager();
+        applicationContext = Mockito.mock(ApplicationContext.class);
         ReflectionTestUtils.setField(namingManager, "applicationContext", 
applicationContext);
         ReflectionTestUtils.setField(namingManager, "heartbeatTimeThreshold", 
500000);
         ReflectionTestUtils.setField(namingManager, 
"heartbeatCheckTimePeriod", 10000000);
         ReflectionTestUtils.setField(namingManager, "metricsManager", new 
NoOpNamingMetricsManager());
 
-        Mockito.when(httpResponse.code()).thenReturn(200);
-        Mockito.when(httpResponse.body()).thenReturn(responseBody);
-        mockedHttpClientUtil = Mockito.mockStatic(HttpClientUtil.class);
-        mockedHttpClientUtil
-                .when(() -> HttpClientUtil.doGet(anyString(), anyMap(), 
anyMap(), anyInt()))
-                .thenReturn(httpResponse);
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        server = MockRestServiceServer.bindTo(restClientBuilder).build();
+        ReflectionTestUtils.setField(namingManager, "restClient", 
restClientBuilder.build());
 
         namingManager.init();
     }
@@ -109,9 +104,7 @@ class NamingManagerTest {
 
     @AfterEach
     void tearDown() {
-        if (mockedHttpClientUtil != null) {
-            mockedHttpClientUtil.close();
-        }
+        server.reset();
     }
 
     @Test
@@ -235,17 +228,19 @@ class NamingManagerTest {
         node.getMetadata().put(CONSTANT_GROUP, vGroups);
         namingManager.registerInstance(node, namespace, clusterName, unitName);
 
-        Mockito.when(httpResponse.code()).thenReturn(200);
         Result<String> result = namingManager.createGroup(namespace, vGroup, 
clusterName, unitName);
         assertFalse(result.isSuccess());
-        vGroup = "test-vGroup2";
-        result = namingManager.createGroup(namespace, vGroup, clusterName, 
unitName);
+        String newVGroup = "test-vGroup2";
+        server.expect(
+                        once(),
+                        request -> assertControlRequest(
+                                request, "/vgroup/v1/addVGroup", "127.0.0.1", 
9080, newVGroup, unitName))
+                .andRespond(withSuccess("", MediaType.APPLICATION_JSON));
+        result = namingManager.createGroup(namespace, newVGroup, clusterName, 
unitName);
         assertTrue(result.isSuccess());
         assertEquals("200", result.getCode());
         assertEquals("add vGroup successfully!", result.getMessage());
-
-        mockedHttpClientUtil.verify(
-                () -> HttpClientUtil.doGet(anyString(), anyMap(), anyMap(), 
anyInt()), Mockito.times(1));
+        server.verify();
     }
 
     @Test
@@ -299,21 +294,18 @@ class NamingManagerTest {
         nodeList.add(node);
         unit.setNamingInstanceList(nodeList);
 
-        Mockito.when(httpResponse.code()).thenReturn(200);
-        Mockito.when(httpResponse.body()).thenReturn(responseBody);
-
-        mockedHttpClientUtil
-                .when(() -> HttpClientUtil.doGet(anyString(), anyMap(), 
anyMap(), anyInt()))
-                .thenReturn(httpResponse);
+        server.expect(
+                        once(),
+                        request -> assertControlRequest(
+                                request, "/vgroup/v1/removeVGroup", 
"127.0.0.1", 9000, vGroup, unitName))
+                .andRespond(withSuccess("", MediaType.APPLICATION_JSON));
 
         Result<String> result = namingManager.removeGroup(unit, vGroup, 
clusterName, namespace, unitName);
 
         assertTrue(result.isSuccess());
         assertEquals("200", result.getCode());
         assertEquals("remove group in old cluster successfully!", 
result.getMessage());
-
-        mockedHttpClientUtil.verify(
-                () -> HttpClientUtil.doGet(anyString(), anyMap(), anyMap(), 
anyInt()), Mockito.times(1));
+        server.verify();
     }
 
     @Test
@@ -435,4 +427,34 @@ class NamingManagerTest {
         ClusterData notFoundNamespace = 
namingManager.getClusterData("non-existent-namespace", clusterName);
         assertNull(notFoundNamespace);
     }
+
+    private void assertControlRequest(
+            org.springframework.http.client.ClientHttpRequest request,
+            String expectedPath,
+            String expectedHost,
+            int expectedPort,
+            String expectedGroup,
+            String expectedUnit) {
+        assertEquals(HttpMethod.GET, request.getMethod());
+        assertEquals(expectedHost, request.getURI().getHost());
+        assertEquals(expectedPort, request.getURI().getPort());
+        assertEquals(expectedPath, request.getURI().getPath());
+        assertEquals(expectedGroup, 
extractQueryValue(request.getURI().getRawQuery(), CONSTANT_GROUP));
+        assertEquals(
+                expectedUnit, 
extractQueryValue(request.getURI().getRawQuery(), 
NamingServerConstants.CONSTANT_UNIT));
+        assertEquals("application/x-www-form-urlencoded", 
request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
+    }
+
+    private String extractQueryValue(String query, String key) {
+        if (query == null) {
+            return null;
+        }
+        for (String pair : query.split("&")) {
+            String[] parts = pair.split("=", 2);
+            if (parts.length == 2 && key.equals(parts[0])) {
+                return parts[1];
+            }
+        }
+        return null;
+    }
 }
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilterTest.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilterTest.java
index 0efa2da41c..77eaf6b2a0 100644
--- 
a/namingserver/src/test/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilterTest.java
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/filter/ConsoleRemotingFilterTest.java
@@ -22,40 +22,55 @@ import 
org.apache.seata.common.metadata.namingserver.NamingServerNode;
 import org.apache.seata.namingserver.manager.NamingManager;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.mock.http.client.MockClientHttpRequest;
+import org.springframework.mock.http.client.MockClientHttpResponse;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.web.client.RestTemplate;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.springframework.test.web.client.ExpectedCount.once;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.headerDoesNotExist;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
 
 /**
  * Unit tests for {@link ConsoleRemotingFilter}.
  * <p>
- * Covers the GET/HEAD body-stripping regression (OkHttp 
IllegalArgumentException)
+ * Covers the GET/HEAD body-stripping regression on upstream proxy requests
  * and other proxy-forwarding behavior.
  */
+// HttpMethod/MediaType static constants and NullBodyClientHttpRequestFactory 
inner anonymous class methods
+// are treated as @Nullable by JDT nullness analysis; they are never null at 
runtime,
+// so the type-safety warning is suppressed here uniformly.
+@SuppressWarnings("null")
 class ConsoleRemotingFilterTest {
 
     private NamingManager namingManager;
-    private RestTemplate restTemplate;
+    private RestClient.Builder restClientBuilder;
+    private MockRestServiceServer server;
     private ConsoleRemotingFilter filter;
     private FilterChain filterChain;
 
@@ -67,60 +82,40 @@ class ConsoleRemotingFilterTest {
     @BeforeEach
     void setUp() {
         namingManager = mock(NamingManager.class);
-        restTemplate = mock(RestTemplate.class);
+        restClientBuilder = RestClient.builder();
+        server = MockRestServiceServer.bindTo(restClientBuilder).build();
         filterChain = mock(FilterChain.class);
-        filter = new ConsoleRemotingFilter(namingManager, restTemplate);
+        filter = new ConsoleRemotingFilter(namingManager, 
restClientBuilder.build());
 
         // Set up a NamingServerNode with a control endpoint
         NamingServerNode node = new NamingServerNode();
         node.setControl(new Node.Endpoint(TARGET_HOST, TARGET_PORT, "http"));
 
-        when(namingManager.getInstances(NAMESPACE, CLUSTER))
-                .thenReturn(Collections.singletonList(node));
+        when(namingManager.getInstances(NAMESPACE, 
CLUSTER)).thenReturn(Collections.singletonList(node));
     }
 
     /**
      * Regression test: a GET request with a non-empty body should NOT forward
-     * the body to the upstream server (to avoid OkHttp's 
IllegalArgumentException).
+     * the body to the upstream server.
      * The body, Content-Length, and Transfer-Encoding headers must be 
stripped.
      */
     @Test
     void getRequestWithBodyShouldStripBody() throws Exception {
-        // Prepare a GET request with a body (some clients/frameworks may 
attach one)
         MockHttpServletRequest request = createConsoleRequest("GET");
         
request.setContent("{\"key\":\"value\"}".getBytes(StandardCharsets.UTF_8));
         request.addHeader(HttpHeaders.CONTENT_LENGTH, "15");
 
         MockHttpServletResponse response = new MockHttpServletResponse();
 
-        // Stub RestTemplate to return a successful JSON response
-        HttpHeaders responseHeaders = new HttpHeaders();
-        responseHeaders.set(HttpHeaders.CONTENT_TYPE, 
"application/json;charset=UTF-8");
-        ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
-                "{\"result\":\"ok\"}".getBytes(StandardCharsets.UTF_8),
-                responseHeaders,
-                HttpStatus.OK);
-
-        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), 
any(HttpEntity.class), eq(byte[].class)))
-                .thenReturn(upstreamResponse);
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.GET))
+                .andExpect(headerDoesNotExist(HttpHeaders.CONTENT_LENGTH))
+                .andExpect(headerDoesNotExist(HttpHeaders.TRANSFER_ENCODING))
+                .andExpect(this::expectEmptyBody)
+                .andRespond(withSuccess("{\"result\":\"ok\"}", 
org.springframework.http.MediaType.APPLICATION_JSON));
 
         filter.doFilter(request, response, filterChain);
-
-        // Capture the HttpEntity sent to RestTemplate
-        @SuppressWarnings("unchecked")
-        ArgumentCaptor<HttpEntity<byte[]>> entityCaptor = 
ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate).exchange(any(URI.class), eq(HttpMethod.GET), 
entityCaptor.capture(), eq(byte[].class));
-
-        HttpEntity<byte[]> capturedEntity = entityCaptor.getValue();
-        // Body must be null (stripped for GET)
-        assertNull(capturedEntity.getBody(), "GET request body should be 
stripped (null)");
-        // Content-Length and Transfer-Encoding headers must not be forwarded
-        assertNull(capturedEntity.getHeaders().get(HttpHeaders.CONTENT_LENGTH),
-                "Content-Length header should be removed for GET");
-        
assertNull(capturedEntity.getHeaders().get(HttpHeaders.TRANSFER_ENCODING),
-                "Transfer-Encoding header should be removed for GET");
-
-        // Verify filterChain was NOT invoked (proxied)
+        server.verify();
         verify(filterChain, never()).doFilter(any(), any());
         assertEquals(200, response.getStatus());
     }
@@ -135,21 +130,13 @@ class ConsoleRemotingFilterTest {
 
         MockHttpServletResponse response = new MockHttpServletResponse();
 
-        HttpHeaders responseHeaders = new HttpHeaders();
-        responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json");
-        ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
-                null, responseHeaders, HttpStatus.OK);
-
-        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.HEAD), 
any(HttpEntity.class), eq(byte[].class)))
-                .thenReturn(upstreamResponse);
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.HEAD))
+                .andExpect(this::expectEmptyBody)
+                
.andRespond(withStatus(HttpStatus.OK).contentType(org.springframework.http.MediaType.APPLICATION_JSON));
 
         filter.doFilter(request, response, filterChain);
-
-        @SuppressWarnings("unchecked")
-        ArgumentCaptor<HttpEntity<byte[]>> entityCaptor = 
ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate).exchange(any(URI.class), eq(HttpMethod.HEAD), 
entityCaptor.capture(), eq(byte[].class));
-
-        assertNull(entityCaptor.getValue().getBody(), "HEAD request body 
should be stripped (null)");
+        server.verify();
         verify(filterChain, never()).doFilter(any(), any());
     }
 
@@ -164,27 +151,62 @@ class ConsoleRemotingFilterTest {
 
         MockHttpServletResponse response = new MockHttpServletResponse();
 
-        HttpHeaders responseHeaders = new HttpHeaders();
-        responseHeaders.set(HttpHeaders.CONTENT_TYPE, 
"application/json;charset=UTF-8");
-        ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
-                "{\"result\":\"created\"}".getBytes(StandardCharsets.UTF_8),
-                responseHeaders,
-                HttpStatus.OK);
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.POST))
+                .andExpect(clientHttpRequest -> assertEquals(
+                        new String(bodyBytes, StandardCharsets.UTF_8),
+                        new String(
+                                ((MockClientHttpRequest) 
clientHttpRequest).getBodyAsBytes(), StandardCharsets.UTF_8),
+                        "POST request body should be forwarded as-is"))
+                .andRespond(
+                        withSuccess("{\"result\":\"created\"}", 
org.springframework.http.MediaType.APPLICATION_JSON));
 
-        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.POST), 
any(HttpEntity.class), eq(byte[].class)))
-                .thenReturn(upstreamResponse);
+        filter.doFilter(request, response, filterChain);
+        server.verify();
+    }
+
+    @Test
+    void putRequestShouldForwardBody() throws Exception {
+        byte[] bodyBytes = 
"{\"data\":\"test-put\"}".getBytes(StandardCharsets.UTF_8);
+        MockHttpServletRequest request = createConsoleRequest("PUT");
+        request.setContent(bodyBytes);
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.PUT))
+                .andExpect(clientHttpRequest -> assertEquals(
+                        new String(bodyBytes, StandardCharsets.UTF_8),
+                        new String(
+                                ((MockClientHttpRequest) 
clientHttpRequest).getBodyAsBytes(), StandardCharsets.UTF_8),
+                        "PUT request body should be forwarded as-is"))
+                .andRespond(
+                        withSuccess("{\"result\":\"updated\"}", 
org.springframework.http.MediaType.APPLICATION_JSON));
 
         filter.doFilter(request, response, filterChain);
+        server.verify();
+    }
+
+    @Test
+    void deleteRequestShouldForwardBody() throws Exception {
+        byte[] bodyBytes = 
"{\"data\":\"test-delete\"}".getBytes(StandardCharsets.UTF_8);
+        MockHttpServletRequest request = createConsoleRequest("DELETE");
+        request.setContent(bodyBytes);
 
-        @SuppressWarnings("unchecked")
-        ArgumentCaptor<HttpEntity<byte[]>> entityCaptor = 
ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate).exchange(any(URI.class), eq(HttpMethod.POST), 
entityCaptor.capture(), eq(byte[].class));
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.DELETE))
+                .andExpect(clientHttpRequest -> assertEquals(
+                        new String(bodyBytes, StandardCharsets.UTF_8),
+                        new String(
+                                ((MockClientHttpRequest) 
clientHttpRequest).getBodyAsBytes(), StandardCharsets.UTF_8),
+                        "DELETE request body should be forwarded as-is"))
+                .andRespond(
+                        withSuccess("{\"result\":\"deleted\"}", 
org.springframework.http.MediaType.APPLICATION_JSON));
 
-        byte[] capturedBody = entityCaptor.getValue().getBody();
-        assertNotNull(capturedBody, "POST body should not be null");
-        assertEquals(new String(bodyBytes, StandardCharsets.UTF_8),
-                new String(capturedBody, StandardCharsets.UTF_8),
-                "POST request body should be forwarded as-is");
+        filter.doFilter(request, response, filterChain);
+        server.verify();
     }
 
     /**
@@ -198,7 +220,6 @@ class ConsoleRemotingFilterTest {
         filter.doFilter(request, response, filterChain);
 
         verify(filterChain).doFilter(any(), any());
-        verify(restTemplate, never()).exchange(any(URI.class), any(), 
any(HttpEntity.class), eq(byte[].class));
     }
 
     /**
@@ -211,24 +232,38 @@ class ConsoleRemotingFilterTest {
 
         MockHttpServletResponse response = new MockHttpServletResponse();
 
-        HttpHeaders responseHeaders = new HttpHeaders();
-        responseHeaders.set(HttpHeaders.CONTENT_TYPE, "application/json");
-        // Upstream sends HTML disguised as JSON
-        byte[] htmlBody = 
"<html><script>alert('xss')</script></html>".getBytes(StandardCharsets.UTF_8);
-        ResponseEntity<byte[]> upstreamResponse = new ResponseEntity<>(
-                htmlBody, responseHeaders, HttpStatus.OK);
-
-        when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), 
any(HttpEntity.class), eq(byte[].class)))
-                .thenReturn(upstreamResponse);
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.GET))
+                .andRespond(withSuccess(
+                        "<html><script>alert('xss')</script></html>",
+                        org.springframework.http.MediaType.APPLICATION_JSON));
 
         filter.doFilter(request, response, filterChain);
+        server.verify();
 
-        assertEquals(502, response.getStatus(),
-                "Should return 502 when upstream body is not valid JSON");
+        assertEquals(502, response.getStatus(), "Should return 502 when 
upstream body is not valid JSON");
         String body = response.getContentAsString();
         assertEquals("{\"error\":\"Upstream returned invalid response 
body\"}", body);
     }
 
+    @Test
+    void non2xxResponseShouldStillBeProxied() throws Exception {
+        MockHttpServletRequest request = createConsoleRequest("GET");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.GET))
+                .andRespond(withStatus(HttpStatus.BAD_GATEWAY)
+                        
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
+                        .body("{\"error\":\"upstream failed\"}"));
+
+        filter.doFilter(request, response, filterChain);
+        server.verify();
+
+        assertEquals(502, response.getStatus(), "Proxy mode should preserve 
upstream non-2xx status codes");
+        assertEquals("{\"error\":\"upstream failed\"}", 
response.getContentAsString());
+    }
+
     /**
      * Helper: create a MockHttpServletRequest that matches the console URL 
pattern
      * and includes the required namespace/cluster headers.
@@ -239,5 +274,148 @@ class ConsoleRemotingFilterTest {
         request.addHeader("x-seata-cluster", CLUSTER);
         return request;
     }
-}
 
+    private void 
expectEmptyBody(org.springframework.http.client.ClientHttpRequest request) 
throws IOException {
+        assertEquals(
+                0,
+                ((MockClientHttpRequest) request).getBodyAsBytes().length,
+                "GET/HEAD request body should be stripped");
+    }
+
+    /**
+     * Regression test: when the upstream returns 204 No Content (no body),
+     * the proxy should pass through 204 rather than returning 500.
+     * <p>
+     * Background: {@code executeProxyRequest} calls
+     * {@code StreamUtils.copyToByteArray(response.getBody())} for non-HEAD 
requests.
+     * Some HTTP client implementations (e.g. JDK HTTP Client) may return 
{@code null}
+     * from {@code getBody()} for 204/304 no-body responses.
+     * Spring 6.x {@code StreamUtils.copyToByteArray(null)} has an internal 
null guard,
+     * but the defensive coding style should remain consistent with
+     * {@code ConsoleLocalServiceImpl} and {@code ConsoleRemoteServiceImpl},
+     * and guard against future Spring version behaviour changes or other HTTP 
client differences.
+     */
+    @Test
+    void upstreamNoContentShouldReturn204() throws Exception {
+        MockHttpServletRequest request = createConsoleRequest("DELETE");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.DELETE))
+                .andRespond(withStatus(HttpStatus.NO_CONTENT));
+
+        filter.doFilter(request, response, filterChain);
+        server.verify();
+
+        assertEquals(204, response.getStatus(), "Upstream 204 No Content 
should be proxied as 204, not 500");
+        assertEquals(0, response.getContentAsByteArray().length, "204 response 
should have no body");
+    }
+
+    /**
+     * Regression test: force {@code getBody()} to return {@code null} via a 
custom
+     * {@link ClientHttpRequestFactory} to simulate the real behaviour of JDK 
HTTP Client
+     * and similar implementations for no-body responses.
+     * After fix: a null body stream is treated as an empty byte array and 204 
is proxied.
+     * Before fix (if {@code StreamUtils} did not handle null): NPE → caught → 
500.
+     */
+    @Test
+    void upstreamNullBodyStreamShouldReturn204NotServerError() throws 
Exception {
+        // Build a RestClient whose getBody() returns null, simulating extreme 
behaviour
+        // of real HTTP clients for 204 responses.
+        RestClient nullBodyRestClient = RestClient.builder()
+                .requestFactory(new 
NullBodyClientHttpRequestFactory(HttpStatus.NO_CONTENT))
+                .build();
+        ConsoleRemotingFilter filterWithNullBody = new 
ConsoleRemotingFilter(namingManager, nullBodyRestClient);
+
+        MockHttpServletRequest request = createConsoleRequest("GET");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        filterWithNullBody.doFilter(request, response, filterChain);
+
+        assertEquals(204, response.getStatus(), "Upstream 204 with null body 
should be proxied as 204, not 500");
+        assertEquals(0, response.getContentAsByteArray().length, "204 null 
body response should have no response body");
+    }
+
+    /**
+     * Regression test: when the upstream returns 304 Not Modified (no body),
+     * the proxy should pass through 304.
+     */
+    @Test
+    void upstreamNotModifiedShouldReturn304() throws Exception {
+        MockHttpServletRequest request = createConsoleRequest("GET");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/globalSession/query";))
+                .andExpect(method(HttpMethod.GET))
+                .andRespond(withStatus(HttpStatus.NOT_MODIFIED));
+
+        filter.doFilter(request, response, filterChain);
+        server.verify();
+
+        assertEquals(304, response.getStatus(), "Upstream 304 Not Modified 
should be proxied as 304, not 500");
+        assertEquals(0, response.getContentAsByteArray().length, "304 response 
should have no body");
+    }
+
+    /**
+     * A {@link ClientHttpRequestFactory} whose responses always return {@code 
null}
+     * from {@code getBody()}, simulating the behaviour of JDK HTTP Client and 
similar
+     * implementations for no-body responses.
+     */
+    private static final class NullBodyClientHttpRequestFactory implements 
ClientHttpRequestFactory {
+
+        private final HttpStatus status;
+
+        NullBodyClientHttpRequestFactory(HttpStatus status) {
+            this.status = status;
+        }
+
+        @Override
+        public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) 
{
+            return new ClientHttpRequest() {
+                private final HttpHeaders headers = new HttpHeaders();
+
+                @Override
+                public ClientHttpResponse execute() {
+                    // Simulate an upstream response with the given status 
code where getBody() is null.
+                    return new MockClientHttpResponse(new byte[0], status) {
+                        @Override
+                        public InputStream getBody() {
+                            // Force null return to simulate JDK HTTP Client 
behaviour for no-body responses.
+                            return null;
+                        }
+
+                        @Override
+                        public void close() {
+                            // Skip close when getBody() is null to avoid 
NullPointerException.
+                        }
+                    };
+                }
+
+                @Override
+                public HttpHeaders getHeaders() {
+                    return headers;
+                }
+
+                @Override
+                public URI getURI() {
+                    return uri;
+                }
+
+                @Override
+                public HttpMethod getMethod() {
+                    return httpMethod;
+                }
+
+                @Override
+                public Map<String, Object> getAttributes() {
+                    return Collections.emptyMap();
+                }
+
+                @Override
+                public OutputStream getBody() throws IOException {
+                    return OutputStream.nullOutputStream();
+                }
+            };
+        }
+    }
+}
diff --git 
a/namingserver/src/test/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImplTest.java
 
b/namingserver/src/test/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImplTest.java
new file mode 100644
index 0000000000..4a6d71c847
--- /dev/null
+++ 
b/namingserver/src/test/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImplTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.namingserver.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.seata.common.metadata.ClusterRole;
+import org.apache.seata.common.metadata.Node;
+import org.apache.seata.common.metadata.namingserver.NamingServerNode;
+import org.apache.seata.mcp.core.props.NameSpaceDetail;
+import org.apache.seata.mcp.exception.ServiceCallException;
+import org.apache.seata.namingserver.manager.NamingManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
+
+import java.util.HashMap;
+import java.util.List;
+
+import static org.apache.seata.common.Constants.RAFT_GROUP_HEADER;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.client.ExpectedCount.once;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static 
org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static 
org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+class ConsoleLocalServiceImplTest {
+
+    private NamingManager namingManager;
+
+    private MockRestServiceServer server;
+
+    private ConsoleLocalServiceImpl consoleLocalService;
+
+    @BeforeEach
+    void setUp() {
+        namingManager = mock(NamingManager.class);
+        RestClient.Builder restClientBuilder = RestClient.builder();
+        server = MockRestServiceServer.bindTo(restClientBuilder).build();
+        consoleLocalService = new ConsoleLocalServiceImpl(namingManager, 
restClientBuilder.build(), new ObjectMapper());
+    }
+
+    @Test
+    void getCallTCShouldForwardRequestToLeaderNode() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setCluster("default");
+
+        NamingServerNode leaderNode = new NamingServerNode();
+        leaderNode.setRole(ClusterRole.LEADER);
+        leaderNode.setUnit("unit-a");
+        leaderNode.setControl(new Node.Endpoint("127.0.0.1", 7091, "http"));
+        when(namingManager.getInstances("public", 
"default")).thenReturn(List.of(leaderNode));
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("x-extra", "value");
+        HashMap<String, String> queryParams = new HashMap<>();
+        queryParams.put("page", "1");
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/sessions?page=1";))
+                .andExpect(method(HttpMethod.GET))
+                .andExpect(header("x-extra", "value"))
+                .andExpect(header(RAFT_GROUP_HEADER, "unit-a"))
+                .andRespond(withSuccess("local-ok", 
org.springframework.http.MediaType.TEXT_PLAIN));
+
+        String response =
+                consoleLocalService.getCallTC(nameSpaceDetail, 
"/api/v1/console/sessions", null, queryParams, headers);
+
+        server.verify();
+        assertEquals("local-ok", response);
+    }
+
+    @Test
+    void getCallTCWithVgroupShouldForwardToNode() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setvGroup("vgroup-local");
+
+        NamingServerNode vgroupNode = new NamingServerNode();
+        vgroupNode.setControl(new Node.Endpoint("127.0.0.1", 8082, "http"));
+        when(namingManager.getInstancesByVgroupAndNamespace("public", 
"vgroup-local", true))
+                .thenReturn(List.of(vgroupNode));
+
+        HttpHeaders headers = new HttpHeaders();
+        HashMap<String, String> queryParams = new HashMap<>();
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:8082/api/v1/console/sessions";))
+                .andExpect(method(HttpMethod.GET))
+                .andRespond(withSuccess("vgroup-ok", 
org.springframework.http.MediaType.TEXT_PLAIN));
+
+        String response =
+                consoleLocalService.getCallTC(nameSpaceDetail, 
"/api/v1/console/sessions", null, queryParams, headers);
+
+        server.verify();
+        assertEquals("vgroup-ok", response);
+    }
+
+    @Test
+    void deleteCallTCShouldThrowWhenUpstreamReturnsNon2xx() {
+        NameSpaceDetail nameSpaceDetail = new NameSpaceDetail();
+        nameSpaceDetail.setNamespace("public");
+        nameSpaceDetail.setCluster("default");
+
+        NamingServerNode node = new NamingServerNode();
+        node.setControl(new Node.Endpoint("127.0.0.1", 7091, "http"));
+        when(namingManager.getInstances("public", 
"default")).thenReturn(List.of(node));
+
+        server.expect(once(), 
requestTo("http://127.0.0.1:7091/api/v1/console/sessions";))
+                .andExpect(method(HttpMethod.DELETE))
+                .andRespond(withStatus(HttpStatus.BAD_GATEWAY).body("failed"));
+
+        assertThrows(
+                ServiceCallException.class,
+                () -> consoleLocalService.deleteCallTC(
+                        nameSpaceDetail, "/api/v1/console/sessions", null, new 
HashMap<>(), new HttpHeaders()));
+        server.verify();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to