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

JNSimba pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 712f372664c [improve](fe) Support _query_plan api forwarding to master 
(#69182)
712f372664c is described below

commit 712f372664cc09bb230f0ca0e12fded9ba90973d
Author: wudi <[email protected]>
AuthorDate: Tue Oct 20 10:03:32 2026 +0800

    [improve](fe) Support _query_plan api forwarding to master (#69182)
    
    ### What problem does this PR solve?
    
    The `_query_plan` API always plans on the receiving FE. Callers that
    need a plan based on the master's metadata currently have to locate and
    contact the master themselves.
    
    Add optional `?forward=true` support using the existing forwarding
    helper. A non-master FE forwards the request to the master; omitted or
    false values retain local planning, and the master handles the request
    locally. Both existing GET and POST entry points remain supported.
    Forwarded query-plan requests use POST internally to preserve the SQL
    body for GET callers. Other APIs retain their existing forwarding
    method.
---
 .../doris/httpv2/rest/RestBaseController.java      |   6 +-
 .../doris/httpv2/rest/TableQueryPlanAction.java    |   4 +
 .../rest/TableQueryPlanActionForwardTest.java      | 150 +++++++++++++++++++++
 3 files changed, 158 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/RestBaseController.java 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/RestBaseController.java
index dff7ac071a4..f68f72f5913 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/RestBaseController.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/RestBaseController.java
@@ -327,6 +327,10 @@ public class RestBaseController extends BaseController {
     }
 
     public Object forwardToMaster(HttpServletRequest request, @Nullable Object 
body) {
+        return forwardToMaster(request, body, request.getMethod());
+    }
+
+    protected Object forwardToMaster(HttpServletRequest request, @Nullable 
Object body, String method) {
         try {
             Env env = Env.getCurrentEnv();
             String redirectUrl = null;
@@ -337,8 +341,6 @@ public class RestBaseController extends BaseController {
                 redirectUrl = HttpURLUtil.buildInternalFeUrl(
                         env.getMasterHost(), request.getRequestURI(), 
request.getQueryString());
             }
-            String method = request.getMethod();
-
             HttpHeaders headers = new HttpHeaders();
             for (String headerName : 
Collections.list(request.getHeaderNames())) {
                 // remove Content-Length because RestTemplate will recalculate 
Content-Length for request body
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableQueryPlanAction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableQueryPlanAction.java
index e20d1560def..8a99c58ca17 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableQueryPlanAction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/TableQueryPlanAction.java
@@ -119,6 +119,10 @@ public class TableQueryPlanAction extends 
RestBaseController {
 
         try {
             String postContent = HttpUtils.getBody(request);
+            if ("true".equals(request.getParameter("forward")) && 
checkForwardToMaster(request)) {
+                // Use POST internally so forwarding a GET request preserves 
the SQL body.
+                return forwardToMaster(request, postContent, "POST");
+            }
             // may be these common validate logic should be moved to one base 
class
             if (Strings.isNullOrEmpty(postContent)) {
                 return ResponseEntityBuilder.badRequest("POST body must 
contains [sql] root object");
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/TableQueryPlanActionForwardTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/TableQueryPlanActionForwardTest.java
new file mode 100644
index 00000000000..59e2b905f6f
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/httpv2/rest/TableQueryPlanActionForwardTest.java
@@ -0,0 +1,150 @@
+// 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.doris.httpv2.rest;
+
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.httpv2.entity.ResponseBody;
+import org.apache.doris.httpv2.exception.UnauthorizedException;
+
+import com.sun.net.httpserver.HttpServer;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import okhttp3.Credentials;
+import org.json.simple.JSONValue;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.springframework.http.ResponseEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TableQueryPlanActionForwardTest {
+    private final Env env = Mockito.mock(Env.class);
+    private final TableQueryPlanAction action = Mockito.spy(new 
TableQueryPlanAction());
+    private final HttpServletRequest request = 
Mockito.mock(HttpServletRequest.class);
+    private final HttpServletResponse response = 
Mockito.mock(HttpServletResponse.class);
+    private MockedStatic<Env> mockedEnv;
+    private boolean originalRunningUnitTest;
+
+    @BeforeEach
+    public void setUp() {
+        originalRunningUnitTest = FeConstants.runningUnitTest;
+        FeConstants.runningUnitTest = true;
+        mockedEnv = Mockito.mockStatic(Env.class);
+        mockedEnv.when(Env::getCurrentEnv).thenReturn(env);
+        Mockito.when(request.getScheme()).thenReturn("http");
+    }
+
+    @AfterEach
+    public void tearDown() {
+        mockedEnv.close();
+        FeConstants.runningUnitTest = originalRunningUnitTest;
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = {"NULL, false", "false, false", "true, true"}, 
nullValues = "NULL")
+    public void testLocalRequestDoesNotForward(String forward, boolean master) 
throws IOException {
+        Mockito.when(env.isMaster()).thenReturn(master);
+        Mockito.when(request.getParameter("forward")).thenReturn(forward);
+        Mockito.doReturn(null).when(action).executeCheckPassword(request, 
response);
+        setRequestBody("{}");
+
+        ResponseEntity<?> result = (ResponseEntity<?>) 
action.query_plan("testDb", "testTbl", request, response);
+
+        Assertions.assertEquals("POST body must contains [sql] root object",
+                ((ResponseBody<?>) result.getBody()).getData());
+        Mockito.verify(action, Mockito.never()).forwardToMaster(Mockito.any(), 
Mockito.any(), Mockito.anyString());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"POST", "GET"})
+    public void testForwardQueryPlan(String method) throws IOException {
+        String sqlBody = "{\"sql\":\"select k1 as `列` from testDb.testTbl\"}";
+        String path = "/api/testDb/testTbl/_query_plan";
+        String rootAuth = Credentials.basic("root", "");
+        HttpServer master = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
+        master.createContext(path, exchange -> {
+            try (exchange) {
+                Map<String, String> received = new HashMap<>();
+                received.put("body", new 
String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
+                received.put("authorization", 
exchange.getRequestHeaders().getFirst("Authorization"));
+                received.put("query", exchange.getRequestURI().getQuery());
+                received.put("method", exchange.getRequestMethod());
+                byte[] result = 
JSONValue.toJSONString(received).getBytes(StandardCharsets.UTF_8);
+                exchange.getResponseHeaders().set("Content-Type", 
"application/json");
+                exchange.sendResponseHeaders(200, result.length);
+                exchange.getResponseBody().write(result);
+            }
+        });
+        master.start();
+        try {
+            Mockito.doReturn(null).when(action).executeCheckPassword(request, 
response);
+            Mockito.when(request.getMethod()).thenReturn(method);
+            Mockito.when(request.getParameter("forward")).thenReturn("true");
+            Mockito.when(request.getRequestURI()).thenReturn(path);
+            Mockito.when(request.getQueryString()).thenReturn("forward=true");
+            Mockito.when(request.getServerName()).thenReturn("127.0.0.1");
+            
Mockito.when(request.getServerPort()).thenReturn(master.getAddress().getPort());
+            Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
+            Mockito.when(request.getHeaderNames()).thenReturn(
+                    Collections.enumeration(List.of("Authorization", 
"Content-Type")));
+            
Mockito.when(request.getHeader("Authorization")).thenReturn(rootAuth);
+            
Mockito.when(request.getHeader("Content-Type")).thenReturn("application/json; 
charset=utf-8");
+            setRequestBody(sqlBody);
+
+            Object result = action.query_plan("testDb", "testTbl", request, 
response);
+
+            Assertions.assertEquals(Map.of("body", sqlBody, "authorization", 
rootAuth,
+                    "query", "forward=true", "method", "POST"), result);
+        } finally {
+            master.stop(0);
+        }
+    }
+
+    @Test
+    public void testForwardRequiresAuthentication() throws IOException {
+        Mockito.when(request.getParameter("forward")).thenReturn("true");
+
+        Assertions.assertThrows(UnauthorizedException.class,
+                () -> action.query_plan("testDb", "testTbl", request, 
response));
+        Mockito.verify(request, Mockito.never()).getInputStream();
+        Mockito.verify(action, Mockito.never()).forwardToMaster(Mockito.any(), 
Mockito.any(), Mockito.anyString());
+    }
+
+    private void setRequestBody(String body) throws IOException {
+        ByteArrayInputStream stream = new 
ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+        ServletInputStream input = Mockito.mock(ServletInputStream.class, 
Mockito.CALLS_REAL_METHODS);
+        Mockito.when(input.read()).thenAnswer(invocation -> stream.read());
+        Mockito.when(request.getInputStream()).thenReturn(input);
+    }
+}


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

Reply via email to