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

SbloodyS pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new c2933f9570 [Fix-18687][HTTP Task] Preserve response bodies for non-200 
status codes (#18688)
c2933f9570 is described below

commit c2933f95700bf36ee06d17b61bd97c0a68cb5cc9
Author: csurong <[email protected]>
AuthorDate: Mon Oct 12 11:47:10 2026 +0800

    [Fix-18687][HTTP Task] Preserve response bodies for non-200 status codes 
(#18688)
---
 docs/docs/en/guide/task/http.md                    |  9 +++
 docs/docs/en/guide/upgrade/incompatible.md         |  1 +
 docs/docs/zh/guide/task/http.md                    |  9 +++
 .../dolphinscheduler/common/utils/OkHttpUtils.java | 11 ++--
 .../common/utils/OkHttpUtilsTest.java              | 16 +++++
 .../plugin/task/http/HttpTaskTest.java             | 76 ++++++++++++++++++++++
 6 files changed, 115 insertions(+), 7 deletions(-)

diff --git a/docs/docs/en/guide/task/http.md b/docs/docs/en/guide/task/http.md
index be2836b0da..7fd76dc6ad 100644
--- a/docs/docs/en/guide/task/http.md
+++ b/docs/docs/en/guide/task/http.md
@@ -22,6 +22,15 @@ This node is used to perform http type tasks and also 
supports http request vali
 | Verification content    | When the verification condition selects a custom 
response code, the content contains, and the content does not contain, the 
verification content is required. |
 | Custom parameter        | It is a user-defined parameter of http part, which 
will replace the content with `${variable}` in the script.                      
                           |
 
+## Response validation
+
+The response body is preserved for all HTTP status codes, including 4xx and 
5xx. A missing body is represented as an empty string.
+
+- Default status validation requires HTTP 200; custom status validation 
requires the configured code.
+- Content-included and content-not-included validation check only the response 
body, without checking the status code. A 4xx or 5xx response can therefore 
pass a body check. Both body checks fail for an empty body.
+
+Choose status-code validation when an error status must fail the task. These 
validation modes are alternatives; selecting a body check does not also enforce 
HTTP 200.
+
 ## Task Output Parameters
 
 | **Task Parameter** |           **Description**           |
diff --git a/docs/docs/en/guide/upgrade/incompatible.md 
b/docs/docs/en/guide/upgrade/incompatible.md
index ad118a1d60..650b52df38 100644
--- a/docs/docs/en/guide/upgrade/incompatible.md
+++ b/docs/docs/en/guide/upgrade/incompatible.md
@@ -46,6 +46,7 @@ This document records the incompatible updates between each 
version. You need to
 
 ## 3.4.3
 
+* HTTP tasks now preserve the real response body for non-200 statuses, 
including 4xx/5xx, instead of a synthetic error string. `BODY_CONTAINS` and 
`BODY_NOT_CONTAINS` evaluate this real body without checking the status code, 
so existing body checks may produce different task results. Use status-code 
validation when error statuses must fail the task. Missing bodies are returned 
as empty strings; both body checks reject empty bodies. 
([#18688](https://github.com/apache/dolphinscheduler/pul [...]
 * Add the `missed_fire_policy` column to `t_ds_schedules`. Existing schedules 
default to `FIRE_ALL_MISSED` to preserve the previous Quartz `IgnoreMisfires` 
behavior. ([#18464](https://github.com/apache/dolphinscheduler/pull/18464))
 * Remove the obsolete Dynamic Task query API. 
([#18556](https://github.com/apache/dolphinscheduler/issues/18556))
 * Remove the obsolete task update-with-upstream API `PUT 
/projects/{projectCode}/task-definition/{code}/with-upstream`. 
([#18568](https://github.com/apache/dolphinscheduler/issues/18568))
diff --git a/docs/docs/zh/guide/task/http.md b/docs/docs/zh/guide/task/http.md
index a9e49a1138..d14f767eb3 100644
--- a/docs/docs/zh/guide/task/http.md
+++ b/docs/docs/zh/guide/task/http.md
@@ -23,6 +23,15 @@
 | 校验内容     | 当校验条件选择自定义响应码、内容包含、内容不包含时,需填写校验内容   |
 | 自定义参数    | 是 http 局部的用户自定义参数,会替换脚本中以 ${变量} 的内容 |
 
+## 响应校验
+
+所有 HTTP 状态码(包括 4xx 和 5xx)的真实响应正文都会保留;缺失的正文用空字符串表示。
+
+- 默认状态码校验要求 HTTP 200,自定义状态码校验要求与配置的状态码一致。
+- “内容包含”和“内容不包含”只检查响应正文,不检查状态码。因此,4xx 或 5xx 响应也可能通过正文校验。正文为空时,这两种校验均失败。
+
+如果错误状态码必须导致任务失败,请选择状态码校验。这些校验模式是互斥选项,选择正文校验不会同时要求 HTTP 200。
+
 ## 任务输出参数
 
 | **任务参数** |       **描述**        |
diff --git 
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
 
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
index 4eef23a6bf..b98353ad1c 100644
--- 
a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
+++ 
b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java
@@ -20,8 +20,6 @@ package org.apache.dolphinscheduler.common.utils;
 import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders;
 import org.apache.dolphinscheduler.common.model.OkHttpResponse;
 
-import org.apache.http.HttpStatus;
-
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.Socket;
@@ -305,11 +303,10 @@ public class OkHttpUtils {
         headers.forEach(requestBuilder::addHeader);
     }
 
-    private static String getResponseBody(@NonNull Response response) throws 
IOException {
-        if (response.code() != HttpStatus.SC_OK || response.body() == null) {
-            return String.format("Request execute failed, httpCode: %s, 
httpBody: %s",
-                    response.code(),
-                    response.body());
+    @VisibleForTesting
+    static String getResponseBody(@NonNull Response response) throws 
IOException {
+        if (response.body() == null) {
+            return "";
         }
         return response.body().string();
     }
diff --git 
a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/OkHttpUtilsTest.java
 
b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/OkHttpUtilsTest.java
index f3a6c94905..f306eb9017 100644
--- 
a/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/OkHttpUtilsTest.java
+++ 
b/dolphinscheduler-common/src/test/java/org/apache/dolphinscheduler/common/utils/OkHttpUtilsTest.java
@@ -29,6 +29,9 @@ import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import okhttp3.OkHttpClient;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -39,6 +42,19 @@ class OkHttpUtilsTest {
 
     private static final int TIMEOUT = 10_000;
 
+    @Test
+    void testAbsentResponseBodyReturnsEmptyString() throws Exception {
+        for (int statusCode : new int[]{200, 204, 400, 500}) {
+            Response response = new Response.Builder()
+                    .request(new 
Request.Builder().url("http://localhost/";).build())
+                    .protocol(Protocol.HTTP_1_1)
+                    .code(statusCode)
+                    .message("test")
+                    .build();
+            Assertions.assertEquals("", OkHttpUtils.getResponseBody(response));
+        }
+    }
+
     @Test
     void testKeepAliveAndPlainClientsAreSeparated() throws Exception {
         OkHttpClient plainClient = OkHttpUtils.getHttpClient(TIMEOUT, TIMEOUT, 
TIMEOUT, false);
diff --git 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-http/src/test/java/org/apache/dolphinscheduler/plugin/task/http/HttpTaskTest.java
 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-http/src/test/java/org/apache/dolphinscheduler/plugin/task/http/HttpTaskTest.java
index 8e0c507cb7..d9491afdf8 100644
--- 
a/dolphinscheduler-task-plugin/dolphinscheduler-task-http/src/test/java/org/apache/dolphinscheduler/plugin/task/http/HttpTaskTest.java
+++ 
b/dolphinscheduler-task-plugin/dolphinscheduler-task-http/src/test/java/org/apache/dolphinscheduler/plugin/task/http/HttpTaskTest.java
@@ -107,6 +107,82 @@ public class HttpTaskTest {
         Assertions.assertEquals(EXIT_CODE_FAILURE, 
httpErrorTask.getExitStatusCode());
     }
 
+    @Test
+    public void testCreatedResponsePreservesOutputBody() throws Exception {
+        String responseBody = "{\"id\":42,\"status\":\"created\"}";
+        HttpTask task = generateHttpTask(DEFAULT_MOCK_PATH, 
HttpRequestMethod.POST, "{\"name\":\"report\"}",
+                new ArrayList<>(), null, 
HttpCheckCondition.STATUS_CODE_CUSTOM, "201",
+                HttpStatus.SC_CREATED, responseBody);
+
+        task.handle(null);
+
+        Assertions.assertEquals("POST", 
getLatestMockWebServer().takeRequest(1, TimeUnit.SECONDS).getMethod());
+        Assertions.assertEquals(EXIT_CODE_SUCCESS, task.getExitStatusCode());
+        String output = task.getParameters().getVarPool().get(0).getValue();
+        Assertions.assertEquals(responseBody, new 
ObjectMapper().readTree(output).get("body").asText());
+    }
+
+    @Test
+    public void testAcceptedResponseChecksActualBody() throws Exception {
+        HttpTask task = generateHttpTask(DEFAULT_MOCK_PATH, 
HttpRequestMethod.POST, "{\"name\":\"report\"}",
+                new ArrayList<>(), null, HttpCheckCondition.BODY_CONTAINS, 
"queued",
+                HttpStatus.SC_ACCEPTED, "{\"id\":42,\"status\":\"queued\"}");
+
+        task.handle(null);
+
+        Assertions.assertEquals(EXIT_CODE_SUCCESS, task.getExitStatusCode());
+    }
+
+    @Test
+    public void testNonOkResponsePreservesBodyAndDefaultStatusCheck() throws 
Exception {
+        for (int statusCode : new int[]{HttpStatus.SC_CREATED, 
HttpStatus.SC_BAD_REQUEST,
+                HttpStatus.SC_INTERNAL_SERVER_ERROR}) {
+            String responseBody = "{\"message\":\"details\"}";
+            HttpTask task = generateHttpTask(DEFAULT_MOCK_PATH, 
HttpRequestMethod.GET, "",
+                    new ArrayList<>(), null, 
HttpCheckCondition.STATUS_CODE_DEFAULT, "",
+                    statusCode, responseBody);
+
+            task.handle(null);
+
+            Assertions.assertEquals(EXIT_CODE_FAILURE, 
task.getExitStatusCode());
+            String output = 
task.getParameters().getVarPool().get(0).getValue();
+            Assertions.assertEquals(responseBody, new 
ObjectMapper().readTree(output).get("body").asText());
+        }
+    }
+
+    @Test
+    public void testErrorResponseBodyChecksUseActualContent() throws Exception 
{
+        for (int statusCode : new int[]{HttpStatus.SC_BAD_REQUEST, 
HttpStatus.SC_INTERNAL_SERVER_ERROR}) {
+            for (HttpCheckCondition check : new 
HttpCheckCondition[]{HttpCheckCondition.BODY_CONTAINS,
+                    HttpCheckCondition.BODY_NOT_CONTAINS}) {
+                for (String body : new String[]{"{\"status\":\"success\"}", 
"{\"status\":\"error\"}", ""}) {
+                    HttpTask task = generateHttpTask(HttpRequestMethod.GET, 
check, "success", statusCode, body);
+                    task.handle(null);
+                    boolean matches = check == HttpCheckCondition.BODY_CONTAINS
+                            ? body.contains("success")
+                            : !body.contains("success");
+                    int expected = !body.isEmpty() && matches ? 
EXIT_CODE_SUCCESS : EXIT_CODE_FAILURE;
+                    Assertions.assertEquals(expected, task.getExitStatusCode(),
+                            "status=" + statusCode + ", check=" + check + ", 
body=" + body);
+                    String output = 
task.getParameters().getVarPool().get(0).getValue();
+                    Assertions.assertEquals(body, new 
ObjectMapper().readTree(output).get("body").asText());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testNoContentResponsePreservesEmptyBody() throws Exception {
+        HttpTask task = generateHttpTask(HttpRequestMethod.DELETE, 
HttpCheckCondition.STATUS_CODE_CUSTOM,
+                "204", HttpStatus.SC_NO_CONTENT, "");
+
+        task.handle(null);
+
+        Assertions.assertEquals(EXIT_CODE_SUCCESS, task.getExitStatusCode());
+        String output = task.getParameters().getVarPool().get(0).getValue();
+        Assertions.assertEquals("", new 
ObjectMapper().readTree(output).get("body").asText());
+    }
+
     @Test
     public void testHandleCheckBodyContains() throws Exception {
         HttpTask httpTask = generateHttpTask(HttpRequestMethod.GET, 
HttpCheckCondition.BODY_CONTAINS,

Reply via email to