This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-12773-7637f3da9b4f19607335c5100b0fe4fd17ddd755 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit a2f818afd2d98c1bc6b281ee2aa11f9138b6851d Author: Supidan <[email protected]> AuthorDate: Thu Oct 15 06:53:50 2026 +0000 [Improve][Connector-V2] Validate HTTP sink URL is not blank (#12773) Co-authored-by: Supidan <[email protected]> --- docs/en/connectors/sink/Feishu.md | 2 +- docs/en/connectors/sink/Http.md | 2 +- docs/zh/connectors/sink/Feishu.md | 2 +- docs/zh/connectors/sink/Http.md | 2 +- .../seatunnel/http/sink/HttpSinkFactory.java | 3 +- .../connectors/seatunnel/http/HttpFactoryTest.java | 54 +++++++++++++++++++++- 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/docs/en/connectors/sink/Feishu.md b/docs/en/connectors/sink/Feishu.md index 155e9dc753..d5d420f7ba 100644 --- a/docs/en/connectors/sink/Feishu.md +++ b/docs/en/connectors/sink/Feishu.md @@ -56,7 +56,7 @@ Feishu webhook URLs and custom authentication headers are sensitive. Do not prin | Name | Type | Required | Default | Description | |-----------------------------|---------|----------|---------|-------------------------------------------------------------------------------------------------------------------------| -| url | String | Yes | - | Feishu webhook URL. The current sink writer sends requests to this fixed URL and does not replace table-name placeholders. | +| url | String | Yes | - | Feishu webhook URL. Must not be empty or whitespace-only. The current sink writer sends requests to this fixed URL and does not replace table-name placeholders. | | headers | Map | No | - | HTTP request headers. Use it when the webhook gateway requires extra headers. | | params | Map | No | - | Accepted by the option rule, but the current sink writer does not pass it to requests. Put non-sensitive query parameters directly in `url` when needed. | | retry | Int | No | - | The maximum retry times when the HTTP request fails with an `IOException`. | diff --git a/docs/en/connectors/sink/Http.md b/docs/en/connectors/sink/Http.md index 3e83b2fc63..9cf60a28a3 100644 --- a/docs/en/connectors/sink/Http.md +++ b/docs/en/connectors/sink/Http.md @@ -38,7 +38,7 @@ They can be downloaded via install-plugin.sh or from the Maven central repositor | Name | Type | Required | Default | Description | |-----------------------------|--------|----------|---------|-------------------------------------------------------------------------------------------------------------| -| url | String | Yes | - | Http request url | +| url | String | Yes | - | HTTP request URL. Must not be empty or whitespace-only. | | headers | Map | No | - | Http headers | | params | Map | No | - | Accepted by the option rule. For the current sink writer, put query parameters directly in `url`; rows are posted to the final URL as the request body. | | retry | Int | No | - | The max retry times if request http return to `IOException` | diff --git a/docs/zh/connectors/sink/Feishu.md b/docs/zh/connectors/sink/Feishu.md index e0735cb579..f3b7543852 100644 --- a/docs/zh/connectors/sink/Feishu.md +++ b/docs/zh/connectors/sink/Feishu.md @@ -56,7 +56,7 @@ import ChangeLog from '../changelog/connector-http-feishu.md'; | 名称 | 类型 | 是否必需 | 默认值 | 描述 | |-----------------------------|---------|------|-------|------------------------------------------------------------------------------------------| -| url | String | 是 | - | 飞书 Webhook URL。当前 Sink 写入器会向这个固定 URL 发送请求,不会替换表名相关占位符。 | +| url | String | 是 | - | 飞书 Webhook URL,不能为空或仅包含空白字符。当前 Sink 写入器会向这个固定 URL 发送请求,不会替换表名相关占位符。 | | headers | Map | 否 | - | HTTP 请求头。Webhook 网关需要额外请求头时使用。 | | params | Map | 否 | - | 该参数会通过参数校验,但当前 Sink 写入器不会把它传入请求。如需查询参数,请把非敏感参数直接写在 `url` 中。 | | retry | Int | 否 | - | HTTP 请求发生 `IOException` 时的最大重试次数。 | diff --git a/docs/zh/connectors/sink/Http.md b/docs/zh/connectors/sink/Http.md index e18718a15b..53140c02ca 100644 --- a/docs/zh/connectors/sink/Http.md +++ b/docs/zh/connectors/sink/Http.md @@ -37,7 +37,7 @@ import ChangeLog from '../changelog/connector-http.md'; | 名称 | 类型 | 是否必须 | 默认值 | 描述 | |-----------------------------|--------|------|-------|------------------------------------------------------------| -| url | String | 是 | - | Http 请求链接 | +| url | String | 是 | - | HTTP 请求链接,不能为空或仅包含空白字符。 | | headers | Map | 否 | - | Http 请求头 | | params | Map | 否 | - | 该参数会通过参数校验。当前 Sink 写入器会把数据作为请求体 POST 到最终 URL,如需查询参数,建议直接写在 `url` 中。 | | retry | Int | 否 | - | 如果请求http返回`IOException`的最大重试次数 | diff --git a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/sink/HttpSinkFactory.java b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/sink/HttpSinkFactory.java index b0a192893b..4fb64a4fdd 100644 --- a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/sink/HttpSinkFactory.java +++ b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/sink/HttpSinkFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.connectors.seatunnel.http.sink; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.options.SinkConnectorCommonOptions; import org.apache.seatunnel.api.table.connector.TableSink; @@ -42,7 +43,7 @@ public class HttpSinkFactory implements TableSinkFactory { @Override public OptionRule optionRule() { return OptionRule.builder() - .required(HttpSinkOptions.URL) + .required(HttpSinkOptions.URL, Conditions.notBlank(HttpSinkOptions.URL)) .optional(HttpSinkOptions.HEADERS) .optional(HttpSinkOptions.PARAMS) .optional(HttpSinkOptions.RETRY) diff --git a/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/HttpFactoryTest.java b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/HttpFactoryTest.java index 44b73c8757..dfca204deb 100644 --- a/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/HttpFactoryTest.java +++ b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/HttpFactoryTest.java @@ -17,17 +17,69 @@ package org.apache.seatunnel.connectors.seatunnel.http; +import org.apache.seatunnel.api.configuration.ReadonlyConfig; +import org.apache.seatunnel.api.configuration.util.ConfigValidator; +import org.apache.seatunnel.api.configuration.util.OptionRule; +import org.apache.seatunnel.api.configuration.util.OptionValidationException; +import org.apache.seatunnel.connectors.seatunnel.http.config.HttpSinkOptions; import org.apache.seatunnel.connectors.seatunnel.http.sink.HttpSinkFactory; import org.apache.seatunnel.connectors.seatunnel.http.source.HttpSourceFactory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; class HttpFactoryTest { + private final OptionRule sinkOptionRule = new HttpSinkFactory().optionRule(); + @Test void optionRule() { Assertions.assertNotNull((new HttpSourceFactory()).optionRule()); - Assertions.assertNotNull((new HttpSinkFactory()).optionRule()); + Assertions.assertNotNull(sinkOptionRule); + } + + @ParameterizedTest + @ValueSource( + strings = { + "http://localhost/test/webhook", + "https://example.com/webhook", + "http://localhost/test/${database_name}_test/${table_name}_test", + "http://localhost/test/${schema_name}_test/${table_name}_test", + " \thttp://localhost/test/webhook\r\n " + }) + void testNonBlankSinkUrlAcceptedWithoutModification(String url) { + ReadonlyConfig config = + ReadonlyConfig.fromMap(Collections.singletonMap(HttpSinkOptions.URL.key(), url)); + + Assertions.assertDoesNotThrow(() -> ConfigValidator.of(config).validate(sinkOptionRule)); + Assertions.assertEquals(url, config.get(HttpSinkOptions.URL)); + } + + @Test + void testMissingSinkUrlRejected() { + ReadonlyConfig config = ReadonlyConfig.fromMap(Collections.emptyMap()); + + OptionValidationException error = + Assertions.assertThrows( + OptionValidationException.class, + () -> ConfigValidator.of(config).validate(sinkOptionRule)); + Assertions.assertTrue(error.getMessage().contains(HttpSinkOptions.URL.key())); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "}) + void testBlankSinkUrlRejected(String url) { + ReadonlyConfig config = + ReadonlyConfig.fromMap(Collections.singletonMap(HttpSinkOptions.URL.key(), url)); + + OptionValidationException error = + Assertions.assertThrows( + OptionValidationException.class, + () -> ConfigValidator.of(config).validate(sinkOptionRule)); + Assertions.assertTrue(error.getMessage().contains(HttpSinkOptions.URL.key())); } }
