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

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


The following commit(s) were added to refs/heads/dev by this push:
     new 62d6804fea [Feature][Connector-V2] Add binary format support for HTTP 
source connector (#10956)
62d6804fea is described below

commit 62d6804fea24d38a857004d6ccb9c39ca07f52c8
Author: cosmosni <[email protected]>
AuthorDate: Sat May 30 13:29:03 2026 +0800

    [Feature][Connector-V2] Add binary format support for HTTP source connector 
(#10956)
---
 docs/en/connectors/source/Http.md                  |  37 +++-
 docs/zh/connectors/source/Http.md                  |  37 +++-
 .../seatunnel/http/client/HttpClientProvider.java  | 206 +++++++++++++++++++++
 .../seatunnel/http/client/HttpResponse.java        |  28 +++
 .../seatunnel/http/config/HttpConfig.java          |   3 +-
 .../seatunnel/http/config/HttpSourceOptions.java   |   7 +
 .../http/exception/HttpConnectorErrorCode.java     |   3 +-
 .../http/source/BinaryResponseCollector.java       |  64 +++++++
 .../seatunnel/http/source/FilenameExtractor.java   |  92 +++++++++
 .../seatunnel/http/source/HttpSource.java          | 149 ++++++++++++---
 .../seatunnel/http/source/HttpSourceReader.java    |  80 +++++++-
 .../http/source/BinaryResponseCollectorTest.java   | 141 ++++++++++++++
 .../http/source/FilenameExtractorTest.java         |  99 ++++++++++
 .../seatunnel/e2e/connector/http/HttpIT.java       |   4 +
 .../src/test/resources/http_binary_to_assert.conf  |  55 ++++++
 .../src/test/resources/mockserver-config.json      |  16 ++
 16 files changed, 985 insertions(+), 36 deletions(-)

diff --git a/docs/en/connectors/source/Http.md 
b/docs/en/connectors/source/Http.md
index b373bef9a8..1af464b2bb 100644
--- a/docs/en/connectors/source/Http.md
+++ b/docs/en/connectors/source/Http.md
@@ -60,7 +60,8 @@ They can be downloaded via install-plugin.sh or from the 
Maven central repositor
 | pageing.cursor_field          | String  | No       | -           | this 
parameter is used to specify the Cursor field name in the request parameter.    
                                                                                
   |
 | pageing.cursor_response_field | String  | No       | -           | This 
parameter specifies the field in the response from which the cursor is 
retrieved.                                                                      
                  |
 | content_field                  | String  | No       | -           | This 
parameter can get some json data.If you only need the data in the 'book' 
section, configure `content_field = "$.store.book.*"`.                          
                |
-| format                        | String  | No       | text        | The 
format of upstream data, now only support `json` `text`, default `text`.        
                                                                                
          |
+| format                        | String  | No       | text        | The 
format of upstream data, supports `json` `text` `binary`, default `text`. When 
set to `binary`, the response body is treated as raw bytes for downloading 
files (PDF, images, ZIP, etc.). |
+| binary_chunk_size             | Long    | No       | 10485760    | Chunk 
size in bytes when `format = binary`. Large files are split into multiple rows. 
Default 10MB. Only effective in BATCH mode.                                     
        |
 | method                        | String  | No       | get         | Http 
request method, only supports GET, POST method.                                 
                                                                                
         |
 | headers                       | Map     | No       | -           | Http 
headers.                                                                        
                                                                                
         |
 | params                        | Map     | No       | -           | Http 
params.                                                                         
                                                                                
         |
@@ -191,6 +192,40 @@ connector will generate data as the following:
 |----------------------------------------------------------|
 | {"code":  200, "data":  "get success", "success":  true} |
 
+when you assign format is `binary`, the HTTP response body is treated as raw 
bytes for downloading files (PDF, images, ZIP, etc.). The output schema is 
fixed as `(data: bytes, relativePath: string, partIndex: long)`. Large files 
are automatically split into multiple rows based on `binary_chunk_size`. Only 
supports BATCH mode.
+
+Example: Download a file via HTTP and write to LocalFileSink:
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  Http {
+    url = "http://example.com/files/report.pdf";
+    method = "GET"
+    format = "binary"
+    binary_chunk_size = 10485760  # 10MB per chunk
+    schema = {
+      fields {
+        data = bytes
+        relativePath = string
+        partIndex = long
+      }
+    }
+  }
+}
+
+sink {
+  LocalFile {
+    path = "/tmp/download"
+    file_format = "binary"
+  }
+}
+```
+
 ### keep_params_as_form
 For compatibility with old versions of http.
 When set to true,`<params>` and `<pageing>` will be submitted in the form.
diff --git a/docs/zh/connectors/source/Http.md 
b/docs/zh/connectors/source/Http.md
index 39cedef67c..5ba01bca56 100644
--- a/docs/zh/connectors/source/Http.md
+++ b/docs/zh/connectors/source/Http.md
@@ -50,7 +50,8 @@ import ChangeLog from '../changelog/connector-http.md';
 | pageing.cursor_field          | String  | 否       | -           | 
此参数用于指定请求参数中的游标字段名称。                                                            
                           |
 | pageing.cursor_response_field | String  | 否       | -           | 
此参数指定从中检索游标的响应字段。                                                               
                             |
 | content_field                  | String  | 否       | -           | 此参数可以获取一些 
json 数据。如果您只需要 'book' 部分的数据,配置 `content_field = "$.store.book.*"`。              
                                |
-| format                        | String  | 否       | text        | 
上游数据的格式,目前仅支持 `json` `text`,默认为 `text`。                                         
                                                             |
+| format                        | String  | 否       | text        | 上游数据的格式,支持 
`json` `text` `binary`,默认为 `text`。当设置为 `binary` 时,响应体作为原始字节处理,用于下载文件(PDF、图片、ZIP 
等)。                                     |
+| binary_chunk_size             | Long    | 否       | 10485760    | 当 `format 
= binary` 时的分片大小(字节)。大文件会被拆分为多行。默认 10MB。仅在 BATCH 模式下生效。                         
                                                    |
 | method                        | String  | 否       | get         | Http 
请求方法,仅支持 GET、POST 方法。                                                           
                                                                   |
 | headers                       | Map     | 否       | -           | Http 头信息。  
                                                                                
                                                                                
   |
 | params                        | Map     | 否       | -           | Http 参数。   
                                                                                
                                                                                
   |
@@ -180,6 +181,40 @@ schema {
 |----------------------------------------------------------|
 | {"code":  200, "data":  "get success", "success":  true} |
 
+当您指定 format 为 `binary` 时,HTTP 响应体作为原始字节处理,用于下载文件(PDF、图片、ZIP 等)。输出 schema 固定为 
`(data: bytes, relativePath: string, partIndex: long)`。大文件会根据 
`binary_chunk_size` 自动拆分为多行。仅支持 BATCH 模式。
+
+示例:通过 HTTP 下载文件并写入 LocalFileSink:
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  Http {
+    url = "http://example.com/files/report.pdf";
+    method = "GET"
+    format = "binary"
+    binary_chunk_size = 10485760  # 每个分片 10MB
+    schema = {
+      fields {
+        data = bytes
+        relativePath = string
+        partIndex = long
+      }
+    }
+  }
+}
+
+sink {
+  LocalFile {
+    path = "/tmp/download"
+    file_format = "binary"
+  }
+}
+```
+
 ### keep_params_as_form
 为了兼容旧版本的 http。
 当设置为 true 时,`<params>` 和 `<pageing>` 将以表单形式提交。
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpClientProvider.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpClientProvider.java
index 5b4075dea5..964a402013 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpClientProvider.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpClientProvider.java
@@ -169,6 +169,191 @@ public class HttpClientProvider implements AutoCloseable {
         return doGet(url, headers, params);
     }
 
+    public HttpResponse executeBinary(
+            String url,
+            String method,
+            Map<String, String> headers,
+            Map<String, String> params,
+            String body,
+            boolean keepParamsAsForm)
+            throws Exception {
+        method = method.toUpperCase(Locale.ROOT);
+        if (HttpGet.METHOD_NAME.equals(method)) {
+            URIBuilder uriBuilder = new URIBuilder(url);
+            addParameters(uriBuilder, params);
+            HttpGet httpGet = new HttpGet(uriBuilder.build());
+            httpGet.setConfig(requestConfig);
+            addHeaders(httpGet, headers);
+            return getResponseBinary(httpGet);
+        }
+        if (HttpPost.METHOD_NAME.equals(method)) {
+            if (keepParamsAsForm) {
+                return doPostBinary(url, headers, params, body);
+            }
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setConfig(requestConfig);
+            addHeaders(httpPost, headers);
+            if (!Strings.isNullOrEmpty(body)) {
+                httpPost.setEntity(new StringEntity(body, 
ContentType.APPLICATION_JSON));
+            } else {
+                addParameters(httpPost, params);
+            }
+            return getResponseBinary(httpPost);
+        }
+        // fallback to GET for other methods
+        URIBuilder uriBuilder = new URIBuilder(url);
+        addParameters(uriBuilder, params);
+        HttpGet httpGet = new HttpGet(uriBuilder.build());
+        httpGet.setConfig(requestConfig);
+        addHeaders(httpGet, headers);
+        return getResponseBinary(httpGet);
+    }
+
+    private HttpResponse doPostBinary(
+            String url, Map<String, String> headers, Map<String, String> 
params, String body)
+            throws Exception {
+        Map<String, Object> bodyMap = new HashMap<>();
+        if (!Strings.isNullOrEmpty(body)) {
+            bodyMap =
+                    ConfigFactory.parseString(body).entrySet().stream()
+                            .collect(
+                                    Collectors.toMap(
+                                            Map.Entry::getKey,
+                                            entry -> 
entry.getValue().unwrapped(),
+                                            (v1, v2) -> v2));
+        }
+        if (MapUtils.isNotEmpty(params)) {
+            headers = MapUtils.isEmpty(headers) ? new HashMap<>() : headers;
+            headers.putIfAbsent(HTTP.CONTENT_TYPE, APPLICATION_FORM);
+        }
+        if (MapUtils.isEmpty(bodyMap)) {
+            bodyMap = new HashMap<>();
+        }
+        bodyMap.putAll(params);
+        URIBuilder uriBuilder = new URIBuilder(url);
+        HttpPost httpPost = new HttpPost(uriBuilder.build());
+        httpPost.setConfig(requestConfig);
+        addHeaders(httpPost, headers);
+        addBody(httpPost, bodyMap);
+        return getResponseBinary(httpPost);
+    }
+
+    /**
+     * Streams binary response directly in chunks without buffering the entire 
body. The consumer is
+     * called once per chunk with a SeaTunnelRow containing (byte[] data, 
String filename, long
+     * partIndex).
+     */
+    public int executeBinaryStreaming(
+            String url,
+            String method,
+            Map<String, String> headers,
+            Map<String, String> params,
+            String body,
+            boolean keepParamsAsForm,
+            long chunkSize,
+            String urlForFilename,
+            java.util.function.Consumer<Object[]> chunkConsumer)
+            throws Exception {
+        HttpRequestBase request =
+                buildRequest(url, method, headers, params, body, 
keepParamsAsForm);
+        try (CloseableHttpResponse response = retryWithException(request)) {
+            if (response == null
+                    || response.getStatusLine() == null
+                    || response.getStatusLine().getStatusCode() >= 300) {
+                int code =
+                        response != null && response.getStatusLine() != null
+                                ? response.getStatusLine().getStatusCode()
+                                : HttpStatus.SC_INTERNAL_SERVER_ERROR;
+                return code;
+            }
+
+            if (response.getEntity() == null) {
+                return response.getStatusLine().getStatusCode();
+            }
+
+            String contentDisposition = null;
+            if (response.getFirstHeader("Content-Disposition") != null) {
+                contentDisposition = 
response.getFirstHeader("Content-Disposition").getValue();
+            }
+            String filename =
+                    
org.apache.seatunnel.connectors.seatunnel.http.source.FilenameExtractor.extract(
+                            contentDisposition, urlForFilename);
+
+            try (java.io.InputStream in = response.getEntity().getContent()) {
+                byte[] buffer = new byte[(int) chunkSize];
+                long partIndex = 0;
+                int bytesRead;
+                while ((bytesRead = in.read(buffer)) > 0) {
+                    byte[] chunk = new byte[bytesRead];
+                    System.arraycopy(buffer, 0, chunk, 0, bytesRead);
+                    chunkConsumer.accept(new Object[] {chunk, filename, 
partIndex});
+                    partIndex++;
+                }
+            }
+            return response.getStatusLine().getStatusCode();
+        }
+    }
+
+    private HttpRequestBase buildRequest(
+            String url,
+            String method,
+            Map<String, String> headers,
+            Map<String, String> params,
+            String body,
+            boolean keepParamsAsForm)
+            throws Exception {
+        method = method.toUpperCase(Locale.ROOT);
+        if (HttpGet.METHOD_NAME.equals(method)) {
+            URIBuilder uriBuilder = new URIBuilder(url);
+            addParameters(uriBuilder, params);
+            HttpGet httpGet = new HttpGet(uriBuilder.build());
+            httpGet.setConfig(requestConfig);
+            addHeaders(httpGet, headers);
+            return httpGet;
+        }
+        if (HttpPost.METHOD_NAME.equals(method)) {
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.setConfig(requestConfig);
+            if (keepParamsAsForm) {
+                Map<String, Object> bodyMap = new HashMap<>();
+                if (!Strings.isNullOrEmpty(body)) {
+                    bodyMap =
+                            ConfigFactory.parseString(body).entrySet().stream()
+                                    .collect(
+                                            Collectors.toMap(
+                                                    Map.Entry::getKey,
+                                                    entry -> 
entry.getValue().unwrapped(),
+                                                    (v1, v2) -> v2));
+                }
+                if (MapUtils.isNotEmpty(params)) {
+                    headers = MapUtils.isEmpty(headers) ? new HashMap<>() : 
headers;
+                    headers.putIfAbsent(HTTP.CONTENT_TYPE, APPLICATION_FORM);
+                }
+                if (MapUtils.isEmpty(bodyMap)) {
+                    bodyMap = new HashMap<>();
+                }
+                bodyMap.putAll(params);
+                addHeaders(httpPost, headers);
+                addBody(httpPost, bodyMap);
+            } else {
+                addHeaders(httpPost, headers);
+                if (!Strings.isNullOrEmpty(body)) {
+                    httpPost.setEntity(new StringEntity(body, 
ContentType.APPLICATION_JSON));
+                } else {
+                    addParameters(httpPost, params);
+                }
+            }
+            return httpPost;
+        }
+        // fallback to GET for other methods
+        URIBuilder uriBuilder = new URIBuilder(url);
+        addParameters(uriBuilder, params);
+        HttpGet httpGet = new HttpGet(uriBuilder.build());
+        httpGet.setConfig(requestConfig);
+        addHeaders(httpGet, headers);
+        return httpGet;
+    }
+
     /**
      * Send a get request without request headers and request parameters
      *
@@ -430,6 +615,27 @@ public class HttpClientProvider implements AutoCloseable {
         return new HttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR);
     }
 
+    private HttpResponse getResponseBinary(HttpRequestBase request) throws 
Exception {
+        try (CloseableHttpResponse httpResponse = retryWithException(request)) 
{
+            if (httpResponse != null && httpResponse.getStatusLine() != null) {
+                byte[] bodyBytes = null;
+                if (httpResponse.getEntity() != null) {
+                    bodyBytes = 
EntityUtils.toByteArray(httpResponse.getEntity());
+                }
+                String contentDisposition = null;
+                if (httpResponse.getFirstHeader("Content-Disposition") != 
null) {
+                    contentDisposition =
+                            
httpResponse.getFirstHeader("Content-Disposition").getValue();
+                }
+                return new HttpResponse(
+                        httpResponse.getStatusLine().getStatusCode(),
+                        bodyBytes,
+                        contentDisposition);
+            }
+        }
+        return new HttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR);
+    }
+
     private CloseableHttpResponse retryWithException(HttpRequestBase request) 
throws Exception {
         return retryer.call(() -> httpClient.execute(request));
     }
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpResponse.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpResponse.java
index cd9d0471ed..cebbb33aeb 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpResponse.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/client/HttpResponse.java
@@ -32,6 +32,12 @@ public class HttpResponse implements Serializable {
     /** response body */
     private String content;
 
+    /** binary response body */
+    private byte[] bodyBytes;
+
+    /** Content-Disposition header value */
+    private String contentDisposition;
+
     public HttpResponse() {}
 
     public HttpResponse(int code) {
@@ -47,6 +53,12 @@ public class HttpResponse implements Serializable {
         this.content = content;
     }
 
+    public HttpResponse(int code, byte[] bodyBytes, String contentDisposition) 
{
+        this.code = code;
+        this.bodyBytes = bodyBytes;
+        this.contentDisposition = contentDisposition;
+    }
+
     public int getCode() {
         return code;
     }
@@ -63,6 +75,22 @@ public class HttpResponse implements Serializable {
         this.content = content;
     }
 
+    public byte[] getBodyBytes() {
+        return bodyBytes;
+    }
+
+    public void setBodyBytes(byte[] bodyBytes) {
+        this.bodyBytes = bodyBytes;
+    }
+
+    public String getContentDisposition() {
+        return contentDisposition;
+    }
+
+    public void setContentDisposition(String contentDisposition) {
+        this.contentDisposition = contentDisposition;
+    }
+
     @Override
     public String toString() {
         return "HttpClientResult [code=" + code + ", content=" + content + "]";
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpConfig.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpConfig.java
index 1801dfd0a7..964e14eea4 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpConfig.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpConfig.java
@@ -23,7 +23,8 @@ public class HttpConfig {
 
     public enum ResponseFormat {
         JSON("json"),
-        TEXT("text");
+        TEXT("text"),
+        BINARY("binary");
 
         private String format;
 
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpSourceOptions.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpSourceOptions.java
index 5014a01c4a..5efce88e09 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpSourceOptions.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/config/HttpSourceOptions.java
@@ -110,6 +110,13 @@ public class HttpSourceOptions extends HttpCommonOptions {
                     .enumType(HttpConfig.ResponseFormat.class)
                     .defaultValue(HttpConfig.ResponseFormat.TEXT)
                     .withDescription("Http response format");
+
+    public static final Option<Long> BINARY_CHUNK_SIZE =
+            Options.key("binary_chunk_size")
+                    .longType()
+                    .defaultValue(10 * 1024 * 1024L)
+                    .withDescription(
+                            "Chunk size in bytes for binary response mode. 
When the response body exceeds this size, it will be split into multiple rows. 
Only effective when format=binary. Default is 10MB.");
     public static final Option<Integer> POLL_INTERVAL_MILLS =
             Options.key("poll_interval_millis")
                     .intType()
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/exception/HttpConnectorErrorCode.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/exception/HttpConnectorErrorCode.java
index bbea694ce9..83254cb3d4 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/exception/HttpConnectorErrorCode.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/exception/HttpConnectorErrorCode.java
@@ -21,7 +21,8 @@ import 
org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
 
 public enum HttpConnectorErrorCode implements SeaTunnelErrorCode {
     FIELD_DATA_IS_INCONSISTENT("HTTP-01", "The field data is inconsistent"),
-    REQUEST_FAILED("HTTP-02", "The request is failed");
+    REQUEST_FAILED("HTTP-02", "The request is failed"),
+    CONFIG_VALIDATION_FAILED("HTTP-03", "The config validation is failed");
 
     private final String code;
     private final String description;
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollector.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollector.java
new file mode 100644
index 0000000000..8fa84e3334
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollector.java
@@ -0,0 +1,64 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.http.source;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.PrimitiveByteArrayType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.connectors.seatunnel.http.client.HttpResponse;
+
+public class BinaryResponseCollector {
+
+    public static final SeaTunnelRowType BINARY_ROW_TYPE =
+            new SeaTunnelRowType(
+                    new String[] {"data", "relativePath", "partIndex"},
+                    new SeaTunnelDataType[] {
+                        PrimitiveByteArrayType.INSTANCE, 
BasicType.STRING_TYPE, BasicType.LONG_TYPE
+                    });
+
+    private BinaryResponseCollector() {}
+
+    public static void collect(
+            HttpResponse response, String url, long chunkSize, 
Collector<SeaTunnelRow> output) {
+        String filename = 
FilenameExtractor.extract(response.getContentDisposition(), url);
+
+        byte[] bodyBytes = response.getBodyBytes();
+        if (bodyBytes == null || bodyBytes.length == 0) {
+            return;
+        }
+
+        if (chunkSize <= 0 || bodyBytes.length <= chunkSize) {
+            output.collect(new SeaTunnelRow(new Object[] {bodyBytes, filename, 
0L}));
+            return;
+        }
+
+        int offset = 0;
+        long partIndex = 0;
+        while (offset < bodyBytes.length) {
+            int end = Math.min(offset + (int) chunkSize, bodyBytes.length);
+            byte[] chunk = new byte[end - offset];
+            System.arraycopy(bodyBytes, offset, chunk, 0, end - offset);
+            output.collect(new SeaTunnelRow(new Object[] {chunk, filename, 
partIndex}));
+            offset = end;
+            partIndex++;
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractor.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractor.java
new file mode 100644
index 0000000000..8511172e4b
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.http.source;
+
+import java.net.URI;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FilenameExtractor {
+
+    private static final Pattern FILENAME_PATTERN =
+            Pattern.compile("filename\\*?=([^;]+)", Pattern.CASE_INSENSITIVE);
+    private static final Pattern ENCODED_FILENAME_PATTERN =
+            Pattern.compile("filename\\*=UTF-8''(.+?)(?:;|$)", 
Pattern.CASE_INSENSITIVE);
+
+    private FilenameExtractor() {}
+
+    public static String extract(String contentDisposition, String url) {
+        // 1. Try Content-Disposition header
+        if (contentDisposition != null && !contentDisposition.isEmpty()) {
+            String filename = parseContentDisposition(contentDisposition);
+            if (filename != null && !filename.isEmpty()) {
+                return filename;
+            }
+        }
+
+        // 2. Try URL path
+        if (url != null && !url.isEmpty()) {
+            String filename = extractFromUrl(url);
+            if (filename != null && !filename.isEmpty()) {
+                return filename;
+            }
+        }
+
+        // 3. Default
+        return "response-body-" + System.currentTimeMillis();
+    }
+
+    static String parseContentDisposition(String contentDisposition) {
+        // Try RFC 5987 encoded filename first (filename*=UTF-8''xxx)
+        Matcher encodedMatcher = 
ENCODED_FILENAME_PATTERN.matcher(contentDisposition);
+        if (encodedMatcher.find()) {
+            try {
+                return java.net.URLDecoder.decode(encodedMatcher.group(1), 
"UTF-8");
+            } catch (java.io.UnsupportedEncodingException e) {
+                return encodedMatcher.group(1);
+            }
+        }
+
+        // Try standard filename="xxx"
+        Matcher matcher = FILENAME_PATTERN.matcher(contentDisposition);
+        if (matcher.find()) {
+            String filename = matcher.group(1).trim();
+            if (filename.startsWith("\"") && filename.endsWith("\"")) {
+                filename = filename.substring(1, filename.length() - 1);
+            }
+            return filename;
+        }
+        return null;
+    }
+
+    static String extractFromUrl(String url) {
+        try {
+            String path = URI.create(url).getPath();
+            if (path != null && !path.isEmpty()) {
+                int lastSlash = path.lastIndexOf('/');
+                String filename = lastSlash >= 0 ? path.substring(lastSlash + 
1) : path;
+                if (!filename.isEmpty() && filename.contains(".")) {
+                    return filename;
+                }
+            }
+        } catch (IllegalArgumentException e) {
+            // URL parsing failed, ignore
+        }
+        return null;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSource.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSource.java
index 44629f4f75..7d577b8dfe 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSource.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSource.java
@@ -33,7 +33,9 @@ import org.apache.seatunnel.api.table.catalog.TableIdentifier;
 import org.apache.seatunnel.api.table.catalog.TablePath;
 import org.apache.seatunnel.api.table.catalog.TableSchema;
 import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.PrimitiveByteArrayType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
 import org.apache.seatunnel.common.constants.JobMode;
 import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated;
 import org.apache.seatunnel.common.utils.JsonUtils;
@@ -58,6 +60,8 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
     protected String contentField;
     protected JobContext jobContext;
     protected DeserializationSchema<SeaTunnelRow> deserializationSchema;
+    protected boolean binaryMode = false;
+    protected long binaryChunkSize = 
HttpSourceOptions.BINARY_CHUNK_SIZE.defaultValue();
 
     protected CatalogTable catalogTable;
 
@@ -72,13 +76,6 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
         return HttpConfig.CONNECTOR_IDENTITY;
     }
 
-    @Override
-    public Boundedness getBoundedness() {
-        return JobMode.BATCH.equals(jobContext.getJobMode())
-                ? Boundedness.BOUNDED
-                : Boundedness.UNBOUNDED;
-    }
-
     private void buildPagingWithConfig(ReadonlyConfig config) {
         Config pluginConfig = config.toConfig();
         if (pluginConfig.hasPath(HttpSourceOptions.PAGEING.key())) {
@@ -145,6 +142,46 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
                         contentField = 
config.getString(HttpSourceOptions.CONTENT_FIELD.key());
                     }
                     break;
+                case BINARY:
+                    this.binaryMode = true;
+                    this.binaryChunkSize = 
pluginConfig.get(HttpSourceOptions.BINARY_CHUNK_SIZE);
+                    SeaTunnelRowType binaryRowType = 
BinaryResponseCollector.BINARY_ROW_TYPE;
+                    TableSchema binarySchema =
+                            TableSchema.builder()
+                                    .column(
+                                            PhysicalColumn.of(
+                                                    "data",
+                                                    
PrimitiveByteArrayType.INSTANCE,
+                                                    0,
+                                                    false,
+                                                    null,
+                                                    null))
+                                    .column(
+                                            PhysicalColumn.of(
+                                                    "relativePath",
+                                                    BasicType.STRING_TYPE,
+                                                    0,
+                                                    false,
+                                                    null,
+                                                    null))
+                                    .column(
+                                            PhysicalColumn.of(
+                                                    "partIndex",
+                                                    BasicType.LONG_TYPE,
+                                                    0,
+                                                    false,
+                                                    null,
+                                                    null))
+                                    .build();
+                    this.catalogTable =
+                            CatalogTable.of(
+                                    TableIdentifier.of(
+                                            HttpConfig.CONNECTOR_IDENTITY, 
TablePath.DEFAULT),
+                                    binarySchema,
+                                    Collections.emptyMap(),
+                                    Collections.emptyList(),
+                                    null);
+                    break;
                 default:
                     // TODO: use format SPI
                     throw new HttpConnectorException(
@@ -154,24 +191,70 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
                                     format));
             }
         } else {
-            TableIdentifier tableIdentifier =
-                    TableIdentifier.of(HttpConfig.CONNECTOR_IDENTITY, 
TablePath.DEFAULT);
-            TableSchema tableSchema =
-                    TableSchema.builder()
-                            .column(
-                                    PhysicalColumn.of(
-                                            "content", BasicType.STRING_TYPE, 
0, false, null, null))
-                            .build();
-
-            this.catalogTable =
-                    CatalogTable.of(
-                            tableIdentifier,
-                            tableSchema,
-                            Collections.emptyMap(),
-                            Collections.emptyList(),
-                            null);
-            this.deserializationSchema =
-                    new 
SimpleTextDeserializationSchema(catalogTable.getSeaTunnelRowType());
+            HttpConfig.ResponseFormat format = 
pluginConfig.get(HttpSourceOptions.FORMAT);
+            if (format == HttpConfig.ResponseFormat.BINARY) {
+                this.binaryMode = true;
+                this.binaryChunkSize = 
pluginConfig.get(HttpSourceOptions.BINARY_CHUNK_SIZE);
+                TableSchema binarySchema =
+                        TableSchema.builder()
+                                .column(
+                                        PhysicalColumn.of(
+                                                "data",
+                                                
PrimitiveByteArrayType.INSTANCE,
+                                                0,
+                                                false,
+                                                null,
+                                                null))
+                                .column(
+                                        PhysicalColumn.of(
+                                                "relativePath",
+                                                BasicType.STRING_TYPE,
+                                                0,
+                                                false,
+                                                null,
+                                                null))
+                                .column(
+                                        PhysicalColumn.of(
+                                                "partIndex",
+                                                BasicType.LONG_TYPE,
+                                                0,
+                                                false,
+                                                null,
+                                                null))
+                                .build();
+                this.catalogTable =
+                        CatalogTable.of(
+                                TableIdentifier.of(
+                                        HttpConfig.CONNECTOR_IDENTITY, 
TablePath.DEFAULT),
+                                binarySchema,
+                                Collections.emptyMap(),
+                                Collections.emptyList(),
+                                null);
+            } else {
+                TableIdentifier tableIdentifier =
+                        TableIdentifier.of(HttpConfig.CONNECTOR_IDENTITY, 
TablePath.DEFAULT);
+                TableSchema tableSchema =
+                        TableSchema.builder()
+                                .column(
+                                        PhysicalColumn.of(
+                                                "content",
+                                                BasicType.STRING_TYPE,
+                                                0,
+                                                false,
+                                                null,
+                                                null))
+                                .build();
+
+                this.catalogTable =
+                        CatalogTable.of(
+                                tableIdentifier,
+                                tableSchema,
+                                Collections.emptyMap(),
+                                Collections.emptyList(),
+                                null);
+                this.deserializationSchema =
+                        new 
SimpleTextDeserializationSchema(catalogTable.getSeaTunnelRowType());
+            }
         }
     }
 
@@ -180,6 +263,18 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
         this.jobContext = jobContext;
     }
 
+    @Override
+    public Boundedness getBoundedness() {
+        if (binaryMode && JobMode.STREAMING.equals(jobContext.getJobMode())) {
+            throw new HttpConnectorException(
+                    CommonErrorCodeDeprecated.ILLEGAL_ARGUMENT,
+                    "HTTP binary format only supports BATCH mode, not 
STREAMING.");
+        }
+        return JobMode.BATCH.equals(jobContext.getJobMode())
+                ? Boundedness.BOUNDED
+                : Boundedness.UNBOUNDED;
+    }
+
     @Override
     public List<CatalogTable> getProducedCatalogTables() {
         return Lists.newArrayList(catalogTable);
@@ -194,7 +289,9 @@ public class HttpSource extends 
AbstractSingleSplitSource<SeaTunnelRow> {
                 this.deserializationSchema,
                 jsonField,
                 contentField,
-                pageInfo);
+                pageInfo,
+                binaryMode,
+                binaryChunkSize);
     }
 
     protected JsonField getJsonField(Config jsonFieldConf) {
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSourceReader.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSourceReader.java
index c7ab3c9c00..2bc66a3796 100644
--- 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSourceReader.java
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/main/java/org/apache/seatunnel/connectors/seatunnel/http/source/HttpSourceReader.java
@@ -75,6 +75,8 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
             Configuration.defaultConfiguration().addOptions(DEFAULT_OPTIONS);
     private boolean noMoreElementFlag = true;
     private Optional<PageInfo> pageInfoOptional = Optional.empty();
+    private final boolean binaryMode;
+    private final long binaryChunkSize;
     /**
      * Holds the original request body template for placeholder replacement. 
This ensures that the
      * state is not unintentionally mutated during pagination.
@@ -87,12 +89,15 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
             DeserializationSchema<SeaTunnelRow> deserializationSchema,
             JsonField jsonField,
             String contentJson) {
-        this.context = context;
-        this.httpParameter = httpParameter;
-        this.deserializationCollector = new 
DeserializationCollector(deserializationSchema);
-        this.jsonField = jsonField;
-        this.contentJson = contentJson;
-        this.rawBody = httpParameter.getBody();
+        this(
+                httpParameter,
+                context,
+                deserializationSchema,
+                jsonField,
+                contentJson,
+                null,
+                false,
+                0L);
     }
 
     public HttpSourceReader(
@@ -102,6 +107,26 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
             JsonField jsonField,
             String contentJson,
             PageInfo pageInfo) {
+        this(
+                httpParameter,
+                context,
+                deserializationSchema,
+                jsonField,
+                contentJson,
+                pageInfo,
+                false,
+                0L);
+    }
+
+    public HttpSourceReader(
+            HttpParameter httpParameter,
+            SingleSplitReaderContext context,
+            DeserializationSchema<SeaTunnelRow> deserializationSchema,
+            JsonField jsonField,
+            String contentJson,
+            PageInfo pageInfo,
+            boolean binaryMode,
+            long binaryChunkSize) {
         this.context = context;
         this.httpParameter = httpParameter;
         this.deserializationCollector = new 
DeserializationCollector(deserializationSchema);
@@ -109,6 +134,15 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
         this.contentJson = contentJson;
         this.pageInfoOptional = Optional.ofNullable(pageInfo);
         this.rawBody = httpParameter.getBody();
+        this.binaryMode = binaryMode;
+        this.binaryChunkSize = binaryChunkSize;
+
+        if (binaryMode && pageInfo != null) {
+            throw new HttpConnectorException(
+                    HttpConnectorErrorCode.CONFIG_VALIDATION_FAILED,
+                    "Binary mode does not support pagination. "
+                            + "Please remove pagination configuration or use 
text mode.");
+        }
     }
 
     @Override
@@ -124,6 +158,10 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
     }
 
     public void pollAndCollectData(Collector<SeaTunnelRow> output) throws 
Exception {
+        if (binaryMode) {
+            pollAndCollectBinaryData(output);
+            return;
+        }
         HttpResponse response = executeRequest();
         if (response.getCode() >= 200 && response.getCode() <= 207) {
             String content = response.getContent();
@@ -153,6 +191,36 @@ public class HttpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
         }
     }
 
+    private void pollAndCollectBinaryData(Collector<SeaTunnelRow> output) 
throws Exception {
+        int statusCode =
+                httpClient.executeBinaryStreaming(
+                        this.httpParameter.getUrl(),
+                        this.httpParameter.getMethod().getMethod(),
+                        this.httpParameter.getHeaders(),
+                        this.httpParameter.getParams(),
+                        this.httpParameter.getBody(),
+                        this.httpParameter.isKeepParamsAsForm(),
+                        binaryChunkSize,
+                        httpParameter.getUrl(),
+                        row -> output.collect(new SeaTunnelRow(row)));
+        if (statusCode >= 200 && statusCode <= 207) {
+            noMoreElementFlag = true;
+        } else {
+            String msg = String.format("http binary request failed, status 
code:[%s]", statusCode);
+            throw new 
HttpConnectorException(HttpConnectorErrorCode.REQUEST_FAILED, msg);
+        }
+    }
+
+    protected HttpResponse executeBinaryRequest() throws Exception {
+        return httpClient.executeBinary(
+                this.httpParameter.getUrl(),
+                this.httpParameter.getMethod().getMethod(),
+                this.httpParameter.getHeaders(),
+                this.httpParameter.getParams(),
+                this.httpParameter.getBody(),
+                this.httpParameter.isKeepParamsAsForm());
+    }
+
     protected HttpResponse executeRequest() throws Exception {
         return httpClient.execute(
                 this.httpParameter.getUrl(),
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollectorTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollectorTest.java
new file mode 100644
index 0000000000..747884729b
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/BinaryResponseCollectorTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.http.source;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.connectors.seatunnel.http.client.HttpResponse;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BinaryResponseCollectorTest {
+
+    private static Collector<SeaTunnelRow> listCollector(List<SeaTunnelRow> 
list) {
+        return new Collector<SeaTunnelRow>() {
+            @Override
+            public void collect(SeaTunnelRow record) {
+                list.add(record);
+            }
+
+            @Override
+            public Object getCheckpointLock() {
+                return this;
+            }
+        };
+    }
+
+    @Test
+    public void testSingleRowOutput() {
+        byte[] data = new byte[1024];
+        HttpResponse response = new HttpResponse(200, data, "attachment; 
filename=\"test.pdf\"");
+        List<SeaTunnelRow> collected = new ArrayList<>();
+
+        BinaryResponseCollector.collect(
+                response, "http://example.com/file";, 10 * 1024 * 1024L, 
listCollector(collected));
+
+        Assertions.assertEquals(1, collected.size());
+        SeaTunnelRow row = collected.get(0);
+        Assertions.assertArrayEquals(data, (byte[]) row.getField(0));
+        Assertions.assertEquals("test.pdf", row.getField(1));
+        Assertions.assertEquals(0L, row.getField(2));
+    }
+
+    @Test
+    public void testChunkedOutput() {
+        byte[] data = new byte[25 * 1024 * 1024]; // 25MB
+        for (int i = 0; i < data.length; i++) {
+            data[i] = (byte) (i % 256);
+        }
+        HttpResponse response = new HttpResponse(200, data, null);
+        List<SeaTunnelRow> collected = new ArrayList<>();
+
+        BinaryResponseCollector.collect(
+                response,
+                "http://example.com/files/big.zip";,
+                10 * 1024 * 1024L,
+                listCollector(collected));
+
+        Assertions.assertEquals(3, collected.size());
+
+        // First chunk: 10MB
+        SeaTunnelRow chunk0 = collected.get(0);
+        Assertions.assertEquals(10 * 1024 * 1024, ((byte[]) 
chunk0.getField(0)).length);
+        Assertions.assertEquals(0L, chunk0.getField(2));
+
+        // Second chunk: 10MB
+        SeaTunnelRow chunk1 = collected.get(1);
+        Assertions.assertEquals(10 * 1024 * 1024, ((byte[]) 
chunk1.getField(0)).length);
+        Assertions.assertEquals(1L, chunk1.getField(2));
+
+        // Third chunk: 5MB
+        SeaTunnelRow chunk2 = collected.get(2);
+        Assertions.assertEquals(5 * 1024 * 1024, ((byte[]) 
chunk2.getField(0)).length);
+        Assertions.assertEquals(2L, chunk2.getField(2));
+
+        // All chunks have same filename
+        String filename = (String) chunk0.getField(1);
+        Assertions.assertEquals("big.zip", filename);
+        Assertions.assertEquals(filename, chunk1.getField(1));
+        Assertions.assertEquals(filename, chunk2.getField(1));
+
+        // Verify data integrity: reassemble and compare
+        byte[] reassembled = new byte[data.length];
+        for (SeaTunnelRow row : collected) {
+            byte[] chunk = (byte[]) row.getField(0);
+            int partIndex = ((Long) row.getField(2)).intValue();
+            System.arraycopy(chunk, 0, reassembled, partIndex * 10 * 1024 * 
1024, chunk.length);
+        }
+        Assertions.assertArrayEquals(data, reassembled);
+    }
+
+    @Test
+    public void testNullBodyBytes() {
+        HttpResponse response = new HttpResponse(200, null, null);
+        List<SeaTunnelRow> collected = new ArrayList<>();
+
+        BinaryResponseCollector.collect(
+                response, "http://example.com/file";, 1024L, 
listCollector(collected));
+
+        Assertions.assertEquals(0, collected.size());
+    }
+
+    @Test
+    public void testEmptyBodyBytes() {
+        HttpResponse response = new HttpResponse(200, new byte[0], null);
+        List<SeaTunnelRow> collected = new ArrayList<>();
+
+        BinaryResponseCollector.collect(
+                response, "http://example.com/file";, 1024L, 
listCollector(collected));
+
+        Assertions.assertEquals(0, collected.size());
+    }
+
+    @Test
+    public void testBinaryRowTypeStructure() {
+        Assertions.assertEquals(3, 
BinaryResponseCollector.BINARY_ROW_TYPE.getTotalFields());
+        Assertions.assertEquals("data", 
BinaryResponseCollector.BINARY_ROW_TYPE.getFieldName(0));
+        Assertions.assertEquals(
+                "relativePath", 
BinaryResponseCollector.BINARY_ROW_TYPE.getFieldName(1));
+        Assertions.assertEquals(
+                "partIndex", 
BinaryResponseCollector.BINARY_ROW_TYPE.getFieldName(2));
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractorTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractorTest.java
new file mode 100644
index 0000000000..34d36609ca
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-base/src/test/java/org/apache/seatunnel/connectors/seatunnel/http/source/FilenameExtractorTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.http.source;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class FilenameExtractorTest {
+
+    @Test
+    public void testParseContentDispositionStandard() {
+        String result =
+                FilenameExtractor.parseContentDisposition("attachment; 
filename=\"report.pdf\"");
+        Assertions.assertEquals("report.pdf", result);
+    }
+
+    @Test
+    public void testParseContentDispositionWithoutQuotes() {
+        String result =
+                FilenameExtractor.parseContentDisposition("attachment; 
filename=report.pdf");
+        Assertions.assertEquals("report.pdf", result);
+    }
+
+    @Test
+    public void testParseContentDispositionUtf8Encoded() {
+        String result =
+                FilenameExtractor.parseContentDisposition(
+                        "attachment; filename*=UTF-8''%E6%8A%A5%E5%91%8A.pdf");
+        Assertions.assertEquals("报告.pdf", result);
+    }
+
+    @Test
+    public void testParseContentDispositionNoFilename() {
+        String result = 
FilenameExtractor.parseContentDisposition("attachment");
+        Assertions.assertNull(result);
+    }
+
+    @Test
+    public void testExtractFromUrlNormalPath() {
+        String result = 
FilenameExtractor.extractFromUrl("http://example.com/files/demo.pdf";);
+        Assertions.assertEquals("demo.pdf", result);
+    }
+
+    @Test
+    public void testExtractFromUrlNoExtension() {
+        String result = 
FilenameExtractor.extractFromUrl("http://example.com/api/download";);
+        Assertions.assertNull(result);
+    }
+
+    @Test
+    public void testExtractFromUrlWithQueryParams() {
+        String result =
+                FilenameExtractor.extractFromUrl(
+                        "http://example.com/files/archive.zip?token=abc123";);
+        Assertions.assertEquals("archive.zip", result);
+    }
+
+    @Test
+    public void testExtractFromUrlTrailingSlash() {
+        String result = 
FilenameExtractor.extractFromUrl("http://example.com/files/";);
+        Assertions.assertNull(result);
+    }
+
+    @Test
+    public void testExtractPriorityContentDispositionOverUrl() {
+        String result =
+                FilenameExtractor.extract(
+                        "attachment; filename=\"report.pdf\"",
+                        "http://example.com/files/other-name.pdf";);
+        Assertions.assertEquals("report.pdf", result);
+    }
+
+    @Test
+    public void testExtractFallbackToUrl() {
+        String result = FilenameExtractor.extract(null, 
"http://example.com/files/demo.zip";);
+        Assertions.assertEquals("demo.zip", result);
+    }
+
+    @Test
+    public void testExtractDefaultFallback() {
+        String result = FilenameExtractor.extract(null, 
"http://example.com/api/download";);
+        Assertions.assertTrue(result.startsWith("response-body-"));
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/java/org/apache/seatunnel/e2e/connector/http/HttpIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/java/org/apache/seatunnel/e2e/connector/http/HttpIT.java
index 30eca3211b..0407e9d203 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/java/org/apache/seatunnel/e2e/connector/http/HttpIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/java/org/apache/seatunnel/e2e/connector/http/HttpIT.java
@@ -365,6 +365,10 @@ public class HttpIT extends TestSuiteBase implements 
TestResource {
         // http airtable source
         Container.ExecResult execResult22 = 
container.executeJob("/airtable_json_to_assert.conf");
         Assertions.assertEquals(0, execResult22.getExitCode());
+
+        // http binary download
+        Container.ExecResult execResult23 = 
container.executeJob("/http_binary_to_assert.conf");
+        Assertions.assertEquals(0, execResult23.getExitCode());
     }
 
     @TestTemplate
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/http_binary_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/http_binary_to_assert.conf
new file mode 100644
index 0000000000..13316adeba
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/http_binary_to_assert.conf
@@ -0,0 +1,55 @@
+#
+# 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.
+#
+
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  Http {
+    plugin_output = "http"
+    url = "http://mockserver:1080/example/binary";
+    method = "GET"
+    format = "binary"
+    schema = {
+      fields {
+        data = bytes
+        relativePath = string
+        partIndex = long
+      }
+    }
+  }
+}
+
+sink {
+  Assert {
+    plugin_input = "http"
+    rules {
+      row_rules = [
+        {
+          rule_type = MAX_ROW
+          rule_value = 1
+        },
+        {
+          rule_type = MIN_ROW
+          rule_value = 1
+        }
+      ]
+    }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/mockserver-config.json
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/mockserver-config.json
index f948a23d2d..6d7a40b199 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/mockserver-config.json
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-http-e2e/src/test/resources/mockserver-config.json
@@ -4943,5 +4943,21 @@
         ]
       }
     }
+  },
+  {
+    "httpRequest": {
+      "method": "GET",
+      "path": "/example/binary"
+    },
+    "httpResponse": {
+      "statusCode": 200,
+      "headers": {
+        "Content-Disposition": "attachment; filename=\"test.pdf\""
+      },
+      "body": {
+        "type": "BINARY",
+        "base64Bytes": "SGVsbG8gV29ybGQgLSBiaW5hcnkgdGVzdCBjb250ZW50"
+      }
+    }
   }
 ]
\ No newline at end of file

Reply via email to