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,