You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/07/27 15:19:57 UTC

(hop) branch main updated: Rest client improvements, fixes #7621 (#7644)

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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new c2c654ad80 Rest client improvements, fixes #7621 (#7644)
c2c654ad80 is described below

commit c2c654ad807b1868dd9cc0774a5ab78516db8b62
Author: Hans Van Akelyen <ha...@gmail.com>
AuthorDate: Mon Jul 27 17:19:51 2026 +0200

    Rest client improvements, fixes #7621 (#7644)
---
 .../ROOT/pages/pipeline/transforms/rest.adoc       |  19 +-
 .../http/0020-rest-connection-query-parameters.hpl | 294 +++++++++++++++++++++
 .../http/0021-rest-connection-headers.hpl          | 294 +++++++++++++++++++++
 ...2-rest-connection-empty-post-content-length.hpl | 282 ++++++++++++++++++++
 .../main-0020-rest-connection-query-parameters.hwf |  97 +++++++
 .../http/main-0021-rest-connection-headers.hwf     |  97 +++++++
 ...2-rest-connection-empty-post-content-length.hwf |  97 +++++++
 .../apache/hop/metadata/rest/RestConnection.java   |  22 ++
 .../rest/RestConnectionContentLengthTest.java      |  98 +++++++
 .../metadata/rest/RestConnectionTimeoutTest.java   | 118 +++++++++
 .../apache/hop/pipeline/transforms/rest/Rest.java  | 151 +++++++++--
 .../hop/pipeline/transforms/rest/RestDialog.java   |  20 +-
 .../rest/messages/messages_en_US.properties        |  20 +-
 .../pipeline/transforms/rest/RestCallRestTest.java | 129 +++++++++
 14 files changed, 1700 insertions(+), 38 deletions(-)

diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc
index 4621c55c4e..cde7ba3dd9 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc
@@ -115,15 +115,16 @@ TIP: To figure out what Headers are required, you can use Postman and remove as
 [options="header"]
 |===
 |Option|Description
-|Field|The field from incoming Hop stream that contains the header information. Add the header value field name to the Field column.
-|Name|The name of the outgoing Hop field from this transform. Add the header key field name to the Name column.
+|Header value field|The field from the incoming Hop stream that contains the header value
+|Header name|The HTTP header name sent to the REST API
 |===
 
-=== Parameters Tab
+=== Query Parameters Tab
 
-The Parameters tab enables you to add URL query parameters to POST, GET, PUT, DELETE, and PATCH requests.
+The Query Parameters tab enables you to add URL query parameters to POST, GET, PUT, DELETE, and PATCH requests.
+Query parameters are added to the end of the URL, e.g. `https://host/path?name1=value1&name2=value2`.
 Each row maps a field from the incoming Hop stream to a query parameter name.
-Click **Get fields** to populate the **Parameter field** list, then enter the parameter name expected by the API in **Parameter name**.
+Click **Get fields** to populate the **Query parameter value field** list, then enter the parameter name expected by the API in **Parameter name**.
 
 For example, if an incoming field named `search_value` contains `apache hop`, mapping it to the parameter name `q` adds `?q=apache+hop` to the request URL.
 Hop URL-encodes the parameter name and value.
@@ -131,21 +132,21 @@ Hop URL-encodes the parameter name and value.
 [options="header"]
 |===
 |Option|Description
-|Parameter field|The field from the incoming Hop stream that contains the parameter value
+|Parameter value field|The field from the incoming Hop stream that contains the parameter value
 |Parameter name|The query parameter name expected by the REST API
 |===
 
 === Matrix Parameters tab
 
 Use the Matrix Parameters tab to define matrix parameter values for POST, PUT, DELETE, and PATCH requests.
-Unlike query parameters, which follow a `?`, matrix parameters are appended to the URL path with a semicolon.
+Unlike query parameters, which follow a `?`, matrix parameters are appended to the URL path with a semicolon, e.g. `https://host/path;name1=value1;name2=value2`.
 For example, a matrix parameter named `language` with the value `en` produces a path such as `/resource;language=en`.
 
 [options="header"]
 |===
 |Option|Description
-|Parameter field|The field from the incoming Hop stream that contains the matrix parameter value
-|Parameter name|The matrix parameter name expected by the REST API
+|Matrix parameter value field|The field from the incoming Hop stream that contains the matrix parameter value
+|Matrix parameter name|The matrix parameter name expected by the REST API
 |===
 
 === Pagination tab
diff --git a/integration-tests/http/0020-rest-connection-query-parameters.hpl b/integration-tests/http/0020-rest-connection-query-parameters.hpl
new file mode 100644
index 0000000000..25e0002467
--- /dev/null
+++ b/integration-tests/http/0020-rest-connection-query-parameters.hpl
@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0020-rest-connection-query-parameters</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies that query parameters are sent when the REST client uses a named connection (issue #7621).</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters>
+      <parameter>
+        <name>HOSTNAME</name>
+        <default_value>localhost</default_value>
+        <description/>
+      </parameter>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/27 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/27 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>check result</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>JSON input</from>
+      <to>check result</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Generate rows</from>
+      <to>REST client</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>REST client</from>
+      <to>JSON input</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <abort_option>ABORT_WITH_ERROR</abort_option>
+    <always_log_rows>Y</always_log_rows>
+    <row_threshold>0</row_threshold>
+    <attributes/>
+    <GUI>
+      <xloc>752</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Generate rows</name>
+    <type>RowGenerator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>parameter</name>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <type>String</type>
+        <nullif>parametervalue</nullif>
+      </field>
+    </fields>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <never_ending>N</never_ending>
+    <limit>1</limit>
+    <row_time_field>now</row_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>JSON input</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>N</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>Y</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>parameter_result</name>
+        <path>$.args</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>result</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>424</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check result</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to/>
+    <send_false_to/>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <conditions>
+          <condition>
+            <negated>Y</negated>
+            <leftvalue>result_status</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>Integer</type>
+              <text>200</text>
+              <length>-1</length>
+              <precision>0</precision>
+              <isnull>N</isnull>
+              <mask>#</mask>
+            </value>
+          </condition>
+          <condition>
+            <negated>Y</negated>
+            <operator>OR</operator>
+            <leftvalue>parameter_result</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>String</type>
+              <text>{"parameter":"parametervalue"}</text>
+              <length>-1</length>
+              <precision>-1</precision>
+              <isnull>N</isnull>
+              <mask/>
+            </value>
+          </condition>
+        </conditions>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>588</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>REST client</name>
+    <type>Rest</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <applicationType>TEXT PLAIN</applicationType>
+    <bodyField/>
+    <connectionTimeout/>
+    <connection_name>castlemock</connection_name>
+    <dynamicMethod>N</dynamicMethod>
+    <headers>
+</headers>
+    <httpLogin/>
+    <httpPassword>Encrypted </httpPassword>
+    <ignoreSsl>N</ignoreSsl>
+    <matrixParameters>
+</matrixParameters>
+    <method>POST</method>
+    <methodFieldName/>
+    <parameters>
+      <parameter>
+        <field>parameter</field>
+        <name>parameter</name>
+      </parameter>
+    </parameters>
+    <preemptive>N</preemptive>
+    <proxyHost/>
+    <proxyPort/>
+    <readTimeout/>
+    <result>
+      <code>result_status</code>
+      <name>result</name>
+      <response_header/>
+      <response_time/>
+    </result>
+    <trustStoreFile/>
+    <trustStorePassword>Encrypted </trustStorePassword>
+    <url>/post</url>
+    <urlField/>
+    <urlInField>N</urlInField>
+    <attributes/>
+    <GUI>
+      <xloc>260</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/http/0021-rest-connection-headers.hpl b/integration-tests/http/0021-rest-connection-headers.hpl
new file mode 100644
index 0000000000..b916f49233
--- /dev/null
+++ b/integration-tests/http/0021-rest-connection-headers.hpl
@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0021-rest-connection-headers</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies that HTTP headers are sent when the REST client uses a named connection (issue #7621).</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters>
+      <parameter>
+        <name>HOSTNAME</name>
+        <default_value>localhost</default_value>
+        <description/>
+      </parameter>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/27 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/27 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>check result</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>JSON input</from>
+      <to>check result</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Generate rows</from>
+      <to>REST client</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>REST client</from>
+      <to>JSON input</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <abort_option>ABORT_WITH_ERROR</abort_option>
+    <always_log_rows>Y</always_log_rows>
+    <row_threshold>0</row_threshold>
+    <attributes/>
+    <GUI>
+      <xloc>752</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Generate rows</name>
+    <type>RowGenerator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>header</name>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+        <type>String</type>
+        <nullif>headervalue</nullif>
+      </field>
+    </fields>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <never_ending>N</never_ending>
+    <limit>1</limit>
+    <row_time_field>now</row_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>JSON input</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>N</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>Y</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>header_parameter</name>
+        <path>$.headers.Header-Parameter</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>result</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>424</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check result</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to/>
+    <send_false_to/>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <conditions>
+          <condition>
+            <negated>Y</negated>
+            <leftvalue>result_status</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>Integer</type>
+              <text>200</text>
+              <length>-1</length>
+              <precision>0</precision>
+              <isnull>N</isnull>
+              <mask>#</mask>
+            </value>
+          </condition>
+          <condition>
+            <negated>Y</negated>
+            <operator>OR</operator>
+            <leftvalue>header_parameter</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>String</type>
+              <text>headervalue</text>
+              <length>-1</length>
+              <precision>-1</precision>
+              <isnull>N</isnull>
+              <mask/>
+            </value>
+          </condition>
+        </conditions>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>588</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>REST client</name>
+    <type>Rest</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <applicationType>TEXT PLAIN</applicationType>
+    <bodyField/>
+    <connectionTimeout/>
+    <connection_name>castlemock</connection_name>
+    <dynamicMethod>N</dynamicMethod>
+    <headers>
+      <header>
+        <field>header</field>
+        <name>header_parameter</name>
+      </header>
+    </headers>
+    <httpLogin/>
+    <httpPassword>Encrypted </httpPassword>
+    <ignoreSsl>N</ignoreSsl>
+    <matrixParameters>
+</matrixParameters>
+    <method>GET</method>
+    <methodFieldName/>
+    <parameters>
+</parameters>
+    <preemptive>N</preemptive>
+    <proxyHost/>
+    <proxyPort/>
+    <readTimeout/>
+    <result>
+      <code>result_status</code>
+      <name>result</name>
+      <response_header/>
+      <response_time/>
+    </result>
+    <trustStoreFile/>
+    <trustStorePassword>Encrypted </trustStorePassword>
+    <url>/get</url>
+    <urlField/>
+    <urlInField>N</urlInField>
+    <attributes/>
+    <GUI>
+      <xloc>260</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/http/0022-rest-connection-empty-post-content-length.hpl b/integration-tests/http/0022-rest-connection-empty-post-content-length.hpl
new file mode 100644
index 0000000000..ebe5f30c1f
--- /dev/null
+++ b/integration-tests/http/0022-rest-connection-empty-post-content-length.hpl
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<pipeline>
+  <info>
+    <name>0022-rest-connection-empty-post-content-length</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Verifies that a body-less POST through a named connection still sends a Content-Length header (issue #7621).</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters>
+      <parameter>
+        <name>HOSTNAME</name>
+        <default_value>localhost</default_value>
+        <description/>
+      </parameter>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/27 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/27 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>check result</from>
+      <to>Abort</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>JSON input</from>
+      <to>check result</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Generate rows</from>
+      <to>REST client</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>REST client</from>
+      <to>JSON input</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Abort</name>
+    <type>Abort</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <abort_option>ABORT_WITH_ERROR</abort_option>
+    <always_log_rows>Y</always_log_rows>
+    <row_threshold>0</row_threshold>
+    <attributes/>
+    <GUI>
+      <xloc>752</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Generate rows</name>
+    <type>RowGenerator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+</fields>
+    <interval_in_ms>5000</interval_in_ms>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <never_ending>N</never_ending>
+    <limit>1</limit>
+    <row_time_field>now</row_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>JSON input</name>
+    <type>JsonInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <addresultfile>N</addresultfile>
+    <readurl>N</readurl>
+    <removeSourceField>N</removeSourceField>
+    <IsIgnoreEmptyFile>N</IsIgnoreEmptyFile>
+    <doNotFailIfNoFile>Y</doNotFailIfNoFile>
+    <ignoreMissingPath>Y</ignoreMissingPath>
+    <defaultPathLeafToNull>Y</defaultPathLeafToNull>
+    <rownum_field/>
+    <file>
+      <name/>
+      <filemask/>
+      <exclude_filemask/>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+    </file>
+    <fields>
+      <field>
+        <name>content_length</name>
+        <path>$.headers.Content-Length</path>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <repeat>N</repeat>
+      </field>
+    </fields>
+    <limit>0</limit>
+    <IsInFields>Y</IsInFields>
+    <IsAFile>N</IsAFile>
+    <valueField>result</valueField>
+    <shortFileFieldName/>
+    <pathFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <extensionFieldName/>
+    <sizeFieldName/>
+    <attributes/>
+    <GUI>
+      <xloc>424</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>check result</name>
+    <type>FilterRows</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <send_true_to/>
+    <send_false_to/>
+    <compare>
+      <condition>
+        <negated>N</negated>
+        <conditions>
+          <condition>
+            <negated>Y</negated>
+            <leftvalue>result_status</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>Integer</type>
+              <text>200</text>
+              <length>-1</length>
+              <precision>0</precision>
+              <isnull>N</isnull>
+              <mask>#</mask>
+            </value>
+          </condition>
+          <condition>
+            <negated>Y</negated>
+            <operator>OR</operator>
+            <leftvalue>content_length</leftvalue>
+            <function>=</function>
+            <rightvalue/>
+            <value>
+              <name>constant</name>
+              <type>String</type>
+              <text>0</text>
+              <length>-1</length>
+              <precision>-1</precision>
+              <isnull>N</isnull>
+              <mask/>
+            </value>
+          </condition>
+        </conditions>
+      </condition>
+    </compare>
+    <attributes/>
+    <GUI>
+      <xloc>588</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>REST client</name>
+    <type>Rest</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <applicationType>TEXT PLAIN</applicationType>
+    <bodyField/>
+    <connectionTimeout/>
+    <connection_name>castlemock</connection_name>
+    <dynamicMethod>N</dynamicMethod>
+    <headers>
+</headers>
+    <httpLogin/>
+    <httpPassword>Encrypted </httpPassword>
+    <ignoreSsl>N</ignoreSsl>
+    <matrixParameters>
+</matrixParameters>
+    <method>POST</method>
+    <methodFieldName/>
+    <parameters>
+</parameters>
+    <preemptive>N</preemptive>
+    <proxyHost/>
+    <proxyPort/>
+    <readTimeout/>
+    <result>
+      <code>result_status</code>
+      <name>result</name>
+      <response_header/>
+      <response_time/>
+    </result>
+    <trustStoreFile/>
+    <trustStorePassword>Encrypted </trustStorePassword>
+    <url>/post</url>
+    <urlField/>
+    <urlInField>N</urlInField>
+    <attributes/>
+    <GUI>
+      <xloc>260</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/http/main-0020-rest-connection-query-parameters.hwf b/integration-tests/http/main-0020-rest-connection-query-parameters.hwf
new file mode 100644
index 0000000000..3c6dacaad0
--- /dev/null
+++ b/integration-tests/http/main-0020-rest-connection-query-parameters.hwf
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0020-rest-connection-query-parameters</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/27 10:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/27 10:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0020-rest-connection-query-parameters.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <clear_files>N</clear_files>
+      <clear_rows>N</clear_rows>
+      <create_parent_folder>N</create_parent_folder>
+      <exec_per_row>N</exec_per_row>
+      <filename>${PROJECT_HOME}/0020-rest-connection-query-parameters.hpl</filename>
+      <logext/>
+      <logfile/>
+      <loglevel>Basic</loglevel>
+      <parameters>
+        <parameter>
+          <name>HOSTNAME</name>
+          <stream_name/>
+          <value>${HOSTNAME}</value>
+        </parameter>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <params_from_previous>N</params_from_previous>
+      <run_configuration>local</run_configuration>
+      <set_append_logfile>N</set_append_logfile>
+      <set_logfile>N</set_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0020-rest-connection-query-parameters.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/http/main-0021-rest-connection-headers.hwf b/integration-tests/http/main-0021-rest-connection-headers.hwf
new file mode 100644
index 0000000000..53d00506e3
--- /dev/null
+++ b/integration-tests/http/main-0021-rest-connection-headers.hwf
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0021-rest-connection-headers</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/27 10:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/27 10:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0021-rest-connection-headers.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <clear_files>N</clear_files>
+      <clear_rows>N</clear_rows>
+      <create_parent_folder>N</create_parent_folder>
+      <exec_per_row>N</exec_per_row>
+      <filename>${PROJECT_HOME}/0021-rest-connection-headers.hpl</filename>
+      <logext/>
+      <logfile/>
+      <loglevel>Basic</loglevel>
+      <parameters>
+        <parameter>
+          <name>HOSTNAME</name>
+          <stream_name/>
+          <value>${HOSTNAME}</value>
+        </parameter>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <params_from_previous>N</params_from_previous>
+      <run_configuration>local</run_configuration>
+      <set_append_logfile>N</set_append_logfile>
+      <set_logfile>N</set_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0021-rest-connection-headers.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/http/main-0022-rest-connection-empty-post-content-length.hwf b/integration-tests/http/main-0022-rest-connection-empty-post-content-length.hwf
new file mode 100644
index 0000000000..5dca9d5da8
--- /dev/null
+++ b/integration-tests/http/main-0022-rest-connection-empty-post-content-length.hwf
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+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.
+
+-->
+<workflow>
+  <name>main-0022-rest-connection-empty-post-content-length</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/27 10:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/27 10:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>50</xloc>
+      <yloc>50</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0022-rest-connection-empty-post-content-length.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <clear_files>N</clear_files>
+      <clear_rows>N</clear_rows>
+      <create_parent_folder>N</create_parent_folder>
+      <exec_per_row>N</exec_per_row>
+      <filename>${PROJECT_HOME}/0022-rest-connection-empty-post-content-length.hpl</filename>
+      <logext/>
+      <logfile/>
+      <loglevel>Basic</loglevel>
+      <parameters>
+        <parameter>
+          <name>HOSTNAME</name>
+          <stream_name/>
+          <value>${HOSTNAME}</value>
+        </parameter>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <params_from_previous>N</params_from_previous>
+      <run_configuration>local</run_configuration>
+      <set_append_logfile>N</set_append_logfile>
+      <set_logfile>N</set_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0022-rest-connection-empty-post-content-length.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/plugins/misc/rest/src/main/java/org/apache/hop/metadata/rest/RestConnection.java b/plugins/misc/rest/src/main/java/org/apache/hop/metadata/rest/RestConnection.java
index 4674dc5a5d..069de61ca1 100644
--- a/plugins/misc/rest/src/main/java/org/apache/hop/metadata/rest/RestConnection.java
+++ b/plugins/misc/rest/src/main/java/org/apache/hop/metadata/rest/RestConnection.java
@@ -192,8 +192,30 @@ public class RestConnection extends HopMetadataBase implements IHopMetadata {
 
   public Invocation.Builder getInvocationBuilder(String url, String proxyHost, Integer proxyPort)
       throws HopException {
+    return getInvocationBuilder(url, proxyHost, proxyPort, 0, 0);
+  }
+
+  /**
+   * Builds a Jersey invocation builder for the given URL. Connect/read timeouts (milliseconds, 0 =
+   * infinite) and proxy settings are applied so that requests made through a REST connection honour
+   * the same timeout, proxy and HTTP-compliance behaviour as the standalone REST transform path
+   * (issue #7621).
+   */
+  public Invocation.Builder getInvocationBuilder(
+      String url, String proxyHost, Integer proxyPort, int connectTimeout, int readTimeout)
+      throws HopException {
     builder = ClientBuilder.newBuilder();
     builder.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
+    builder.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
+    // Only apply timeouts when configured (>= 0). Empty transform timeout fields resolve to -1,
+    // which Jersey rejects with "timeouts can't be negative"; leaving the property unset keeps
+    // Jersey's default (0 = infinite), matching the previous connection-path behaviour.
+    if (connectTimeout >= 0) {
+      builder.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout);
+    }
+    if (readTimeout >= 0) {
+      builder.property(ClientProperties.READ_TIMEOUT, readTimeout);
+    }
     setProxyHost(proxyHost, proxyPort);
 
     // Configure SSL if needed (client cert, trust store, or ignore SSL)
diff --git a/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionContentLengthTest.java b/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionContentLengthTest.java
new file mode 100644
index 0000000000..a6b99564b9
--- /dev/null
+++ b/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionContentLengthTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.hop.metadata.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.sun.net.httpserver.HttpServer;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for issue #7621: a POST issued through a REST connection must send a {@code
+ * Content-Length} header, just like the standalone URL path does.
+ *
+ * <p>The connection path uses Jersey's default (JDK {@code HttpURLConnection}) connector. That
+ * connector omits {@code Content-Length} for a {@code null} request entity, but sends {@code
+ * Content-Length: 0} for an empty (non-null) entity. {@code Rest.executeRequest} therefore
+ * normalizes a null body to an empty string; this test locks in the connector behaviour that fix
+ * relies on: an empty-body POST through a connection carries {@code Content-Length} and is not sent
+ * with chunked transfer encoding.
+ */
+class RestConnectionContentLengthTest {
+
+  private HttpServer server;
+  private String url;
+
+  @BeforeEach
+  void startServer() throws Exception {
+    server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+    // Echo back the request's Content-Length and whether it arrived chunked, so the test can prove
+    // the connection path sends Content-Length rather than using chunked transfer encoding.
+    server.createContext(
+        "/objects",
+        exchange -> {
+          // Drain the request body before responding.
+          exchange.getRequestBody().readAllBytes();
+          String contentLength = exchange.getRequestHeaders().getFirst("Content-Length");
+          List<String> transferEncoding = exchange.getRequestHeaders().get("Transfer-Encoding");
+          boolean chunked =
+              transferEncoding != null
+                  && transferEncoding.stream().anyMatch(v -> v.equalsIgnoreCase("chunked"));
+          byte[] body =
+              ("content-length=" + contentLength + ";chunked=" + chunked)
+                  .getBytes(StandardCharsets.UTF_8);
+          exchange.sendResponseHeaders(200, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    url = "http://localhost:" + server.getAddress().getPort() + "/objects";
+  }
+
+  @AfterEach
+  void stopServer() {
+    if (server != null) {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void emptyBodyPostThroughConnectionSendsContentLength() throws Exception {
+    RestConnection connection = new RestConnection(new Variables());
+
+    Invocation.Builder invocationBuilder = connection.getInvocationBuilder(url);
+    try (Response response =
+        invocationBuilder.post(Entity.entity("", MediaType.APPLICATION_JSON_TYPE))) {
+
+      assertEquals(200, response.getStatus());
+      assertEquals("content-length=0;chunked=false", response.readEntity(String.class));
+    }
+  }
+}
diff --git a/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionTimeoutTest.java b/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionTimeoutTest.java
new file mode 100644
index 0000000000..6a3f1e3e3e
--- /dev/null
+++ b/plugins/misc/rest/src/test/java/org/apache/hop/metadata/rest/RestConnectionTimeoutTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.hop.metadata.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.sun.net.httpserver.HttpServer;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.core.Response;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for issue #7621: the connect/read timeouts configured on the REST transform must
+ * be honoured when the request goes through a REST connection. Before the fix {@link
+ * RestConnection#getInvocationBuilder} never set {@code CONNECT_TIMEOUT}/{@code READ_TIMEOUT}, so a
+ * slow or hung server would block the pipeline indefinitely on the connection path.
+ */
+class RestConnectionTimeoutTest {
+
+  private HttpServer server;
+  private String url;
+  private String fastUrl;
+
+  @BeforeEach
+  void startServer() throws Exception {
+    server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+    // Withhold the response for far longer than the client read timeout below.
+    server.createContext(
+        "/slow",
+        exchange -> {
+          try {
+            Thread.sleep(1500);
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+          }
+          byte[] body = "ok".getBytes(StandardCharsets.UTF_8);
+          try {
+            exchange.sendResponseHeaders(200, body.length);
+            try (OutputStream os = exchange.getResponseBody()) {
+              os.write(body);
+            }
+          } catch (Exception ignore) {
+            // The client has already timed out and disconnected.
+          } finally {
+            exchange.close();
+          }
+        });
+    // Immediate response, used to prove a request still succeeds with unset (negative) timeouts.
+    server.createContext(
+        "/fast",
+        exchange -> {
+          byte[] body = "ok".getBytes(StandardCharsets.UTF_8);
+          exchange.sendResponseHeaders(200, body.length);
+          try (OutputStream os = exchange.getResponseBody()) {
+            os.write(body);
+          }
+        });
+    server.start();
+    url = "http://localhost:" + server.getAddress().getPort() + "/slow";
+    fastUrl = "http://localhost:" + server.getAddress().getPort() + "/fast";
+  }
+
+  @AfterEach
+  void stopServer() {
+    if (server != null) {
+      server.stop(0);
+    }
+  }
+
+  @Test
+  void readTimeoutThroughConnectionIsHonoured() throws Exception {
+    RestConnection connection = new RestConnection(new Variables());
+
+    // connectTimeout 2000 ms, readTimeout 300 ms — the server withholds the response for 1500 ms,
+    // so the read must time out (a ProcessingException wrapping a SocketTimeoutException).
+    Invocation.Builder invocationBuilder =
+        connection.getInvocationBuilder(url, null, 8080, 2000, 300);
+
+    assertThrows(ProcessingException.class, () -> invocationBuilder.get(String.class));
+  }
+
+  @Test
+  void negativeTimeoutsAreIgnored() throws Exception {
+    RestConnection connection = new RestConnection(new Variables());
+
+    // Empty transform timeout fields resolve to -1; the connection must not pass a negative timeout
+    // to Jersey (which throws "timeouts can't be negative"). The request should simply succeed.
+    Invocation.Builder invocationBuilder =
+        connection.getInvocationBuilder(fastUrl, null, 8080, -1, -1);
+
+    try (Response response = invocationBuilder.get()) {
+      assertEquals(200, response.getStatus());
+    }
+  }
+}
diff --git a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
index 6a761bd818..15af5c381d 100644
--- a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
+++ b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
@@ -220,6 +220,60 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
     return ub.build().toString();
   }
 
+  /**
+   * Appends the configured matrix and query parameters (from the current row) to the given URL. The
+   * standalone request path applies these directly to the Jersey {@link WebTarget}; the REST
+   * connection path builds its target inside {@link
+   * org.apache.hop.metadata.rest.RestConnection#getInvocationBuilder}, so we bake the parameters
+   * into the URL here to keep both paths equivalent (issue #7621).
+   */
+  protected String appendMatrixAndQueryParams(String url, Object[] rowData) throws HopException {
+    if (!data.useMatrixParams && !data.useParams) {
+      return url;
+    }
+    UriBuilder builder = UriBuilder.fromUri(URI.create(url));
+    if (data.useMatrixParams) {
+      for (int i = 0; i < data.nrMatrixParams; i++) {
+        String value = data.inputRowMeta.getString(rowData, data.indexOfMatrixParamFields[i]);
+        if (isDebug()) {
+          logDebug(
+              BaseMessages.getString(
+                  PKG, "Rest.Log.matrixParameterValue", data.matrixParamNames[i], value));
+        }
+        builder =
+            builder.matrixParam(
+                data.matrixParamNames[i],
+                UriComponent.encode(value, UriComponent.Type.QUERY_PARAM));
+      }
+    }
+    if (data.useParams) {
+      for (int i = 0; i < data.nrParams; i++) {
+        String value = data.inputRowMeta.getString(rowData, data.indexOfParamFields[i]);
+        if (isDebug()) {
+          logDebug(
+              BaseMessages.getString(
+                  PKG, "Rest.Log.queryParameterValue", data.paramNames[i], value));
+        }
+        builder = builder.queryParam(data.paramNames[i], value);
+      }
+    }
+    return builder.build().toString();
+  }
+
+  /**
+   * Masks the value of security-sensitive headers so credentials are not written to the log when
+   * header values are logged at debug level.
+   */
+  private static String maskHeaderValue(String name, String value) {
+    if (name != null
+        && ("Authorization".equalsIgnoreCase(name)
+            || "Proxy-Authorization".equalsIgnoreCase(name)
+            || "Cookie".equalsIgnoreCase(name))) {
+      return "********";
+    }
+    return value;
+  }
+
   /**
    * Executes a single REST exchange and bundles the deserialized pieces required for paging and row
    * assembly.
@@ -249,13 +303,28 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
     final long httpVolIn0 = dataVolumeIn != null ? dataVolumeIn : 0L;
     final long httpVolOut0 = dataVolumeOut != null ? dataVolumeOut : 0L;
     try {
-      if (isDetailed()) {
-        logDetailed(BaseMessages.getString(PKG, "Rest.Log.ConnectingToURL", effectiveBase));
-      }
+      String requestUri = effectiveBase;
       if (!StringUtils.isEmpty(meta.getConnectionName())) {
+        // The connection builds its own Jersey target internally, so bake the matrix/query
+        // parameters into the URL and pass the timeouts through, to keep this path equivalent to
+        // the standalone path below (issue #7621).
+        requestUri = appendMatrixAndQueryParams(effectiveBase, rowData);
+        if (isDetailed()) {
+          logDetailed(
+              BaseMessages.getString(
+                  PKG, "Rest.Log.UsingConnection", meta.getConnectionName(), NVL(baseUrl, "")));
+        }
         invocationBuilder =
-            connection.getInvocationBuilder(effectiveBase, data.realProxyHost, data.realProxyPort);
+            connection.getInvocationBuilder(
+                requestUri,
+                data.realProxyHost,
+                data.realProxyPort,
+                data.realConnectionTimeout,
+                data.realReadTimeout);
       } else {
+        if (isDetailed()) {
+          logDetailed(BaseMessages.getString(PKG, "Rest.Log.NoConnection"));
+        }
         ClientBuilder clientBuilder = createClientBuilder();
         clientBuilder
             .withConfig(data.config)
@@ -300,14 +369,21 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
             webResource = webResource.queryParam(data.paramNames[i], value);
           }
         }
-        if (isDebug()) {
-          logDebug(BaseMessages.getString(PKG, "Rest.Log.ConnectingToURL", webResource.getUri()));
-        }
         invocationBuilder = webResource.request();
+        requestUri = webResource.getUri().toString();
       }
       if (invocationBuilder == null) {
         throw new HopException("Invocation builder not initialized");
       }
+      if (isDetailed()) {
+        logDetailed(
+            BaseMessages.getString(PKG, "Rest.Log.SendingRequest", data.method, requestUri));
+      }
+      if (isDebug()) {
+        logDebug(
+            BaseMessages.getString(
+                PKG, "Rest.Log.Timeouts", data.realConnectionTimeout, data.realReadTimeout));
+      }
 
       MultivaluedMap<String, Object> headerMap = new MultivaluedHashMap<>();
 
@@ -317,6 +393,22 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
         for (int i = 0; i < data.nrheader; i++) {
           String value = data.inputRowMeta.getString(rowData, data.indexOfHeaderFields[i]);
 
+          // Content-Length is a restricted header computed by the HTTP client when the request
+          // entity is buffered. Setting it manually throws "Content-Length already defined" on
+          // some connectors, so we ignore any user-supplied value and let the client manage it
+          // (issue #7621).
+          if ("Content-Length".equalsIgnoreCase(data.headerNames[i])) {
+            if (isDebug()) {
+              logDebug(
+                  BaseMessages.getString(
+                      PKG,
+                      "Rest.Log.HeaderValue",
+                      data.headerNames[i],
+                      maskHeaderValue(data.headerNames[i], value)));
+            }
+            continue;
+          }
+
           headerMap.putSingle(data.headerNames[i], value);
           if ("Content-Type".equals(data.headerNames[i])) {
             contentType = value;
@@ -326,7 +418,11 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
           }
           if (isDebug()) {
             logDebug(
-                BaseMessages.getString(PKG, "Rest.Log.HeaderValue", data.headerNames[i], value));
+                BaseMessages.getString(
+                    PKG,
+                    "Rest.Log.HeaderValue",
+                    data.headerNames[i],
+                    maskHeaderValue(data.headerNames[i], value)));
           }
         }
       }
@@ -376,6 +472,14 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
       final String finalEntityString = entityString;
       final String finalContentType = contentType;
 
+      if (isDebug()) {
+        logDebug(
+            BaseMessages.getString(
+                PKG,
+                "Rest.Log.RequestContentType",
+                contentType != null ? contentType : String.valueOf(data.mediaType)));
+      }
+
       startTime = System.currentTimeMillis();
       response =
           executeWithRetry(
@@ -394,6 +498,15 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
       long responseTime = System.currentTimeMillis() - startTime;
 
       int status = response.getStatus();
+      if (isDetailed()) {
+        logDetailed(BaseMessages.getString(PKG, "Rest.Log.ResponseCode", status));
+        logDetailed(BaseMessages.getString(PKG, "Rest.Log.ResponseTime", responseTime, requestUri));
+        if (status >= 400) {
+          logDetailed(
+              BaseMessages.getString(
+                  PKG, "Rest.Log.ResponseError", data.method, requestUri, status));
+        }
+      }
 
       if (response.hasEntity()) {
         response.bufferEntity();
@@ -442,6 +555,9 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
         }
       }
       trackResponseBytes(response, body);
+      if (isRowLevel()) {
+        logRowlevel(BaseMessages.getString(PKG, "Rest.Log.ResponseBody", body));
+      }
 
       MultivaluedMap<String, Object> headers = searchForHeaders(response);
       JSONObject json = new JSONObject();
@@ -1212,6 +1328,11 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
   private Response executeRequest(
       Invocation.Builder invocationBuilder, String entityString, String contentType)
       throws HopException {
+    // A body-less POST/PUT/PATCH/DELETE must still carry a Content-Length header (issue #7621).
+    // The JDK HttpURLConnection connector used by the REST-connection path omits Content-Length for
+    // a null entity, whereas the standalone (Apache) connector sends Content-Length: 0. Normalizing
+    // a null body to an empty string makes both request paths send Content-Length consistently.
+    String body = entityString == null ? "" : entityString;
     try {
       switch (data.method) {
         case RestMeta.HTTP_METHOD_GET -> {
@@ -1222,9 +1343,9 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
               entityString,
               contentType != null ? resolveCharset(contentType) : resolveCharset(data.mediaType));
           if (null != contentType) {
-            return invocationBuilder.post(Entity.entity(entityString, contentType));
+            return invocationBuilder.post(Entity.entity(body, contentType));
           } else {
-            return invocationBuilder.post(Entity.entity(entityString, data.mediaType));
+            return invocationBuilder.post(Entity.entity(body, data.mediaType));
           }
         }
         case RestMeta.HTTP_METHOD_PUT -> {
@@ -1232,9 +1353,9 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
               entityString,
               contentType != null ? resolveCharset(contentType) : resolveCharset(data.mediaType));
           if (null != contentType) {
-            return invocationBuilder.put(Entity.entity(entityString, contentType));
+            return invocationBuilder.put(Entity.entity(body, contentType));
           } else {
-            return invocationBuilder.put(Entity.entity(entityString, data.mediaType));
+            return invocationBuilder.put(Entity.entity(body, data.mediaType));
           }
         }
         case RestMeta.HTTP_METHOD_DELETE -> {
@@ -1242,7 +1363,7 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
               entityString,
               contentType != null ? resolveCharset(contentType) : resolveCharset(data.mediaType));
           Invocation invocation =
-              invocationBuilder.build("DELETE", Entity.entity(entityString, data.mediaType));
+              invocationBuilder.build("DELETE", Entity.entity(body, data.mediaType));
           return invocation.invoke();
         }
         case RestMeta.HTTP_METHOD_HEAD -> {
@@ -1257,10 +1378,10 @@ public class Rest extends BaseTransform<RestMeta, RestData> {
               contentType != null ? resolveCharset(contentType) : resolveCharset(data.mediaType));
           if (null != contentType) {
             return invocationBuilder.method(
-                RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString, contentType));
+                RestMeta.HTTP_METHOD_PATCH, Entity.entity(body, contentType));
           } else {
             return invocationBuilder.method(
-                RestMeta.HTTP_METHOD_PATCH, Entity.entity(entityString, data.mediaType));
+                RestMeta.HTTP_METHOD_PATCH, Entity.entity(body, data.mediaType));
           }
         }
         default ->
diff --git a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
index 83364eb56e..593c383922 100644
--- a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
+++ b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/RestDialog.java
@@ -677,11 +677,12 @@ public class RestDialog extends BaseTransformDialog {
       int margin,
       CTabItem wMatrixParametersTab,
       Composite wMatrixParametersComp) {
-    wlMatrixParameters = new Label(wMatrixParametersComp, SWT.NONE);
-    wlMatrixParameters.setText(BaseMessages.getString(PKG, "RestDialog.Parameters.Label"));
+    wlMatrixParameters = new Label(wMatrixParametersComp, SWT.LEFT | SWT.WRAP);
+    wlMatrixParameters.setText(BaseMessages.getString(PKG, "RestDialog.MatrixParameters.Info"));
     PropsUi.setLook(wlMatrixParameters);
     FormData fdlMatrixParameters = new FormData();
     fdlMatrixParameters.left = new FormAttachment(0, 0);
+    fdlMatrixParameters.right = new FormAttachment(100, -margin);
     fdlMatrixParameters.top = new FormAttachment(wSpacer, margin);
     wlMatrixParameters.setLayoutData(fdlMatrixParameters);
 
@@ -697,12 +698,12 @@ public class RestDialog extends BaseTransformDialog {
     matrixParameterColumns =
         new ColumnInfo[] {
           new ColumnInfo(
-              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterField"),
+              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.MatrixParameterValueField"),
               ColumnInfo.COLUMN_TYPE_CCOMBO,
               new String[] {""},
               false),
           new ColumnInfo(
-              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterName"),
+              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.MatrixParameterName"),
               ColumnInfo.COLUMN_TYPE_TEXT,
               false),
         };
@@ -742,11 +743,12 @@ public class RestDialog extends BaseTransformDialog {
 
   private void setupParameterTabContent(
       ModifyListener lsMod, int margin, CTabItem wParametersTab, Composite wParametersComp) {
-    wlParameters = new Label(wParametersComp, SWT.NONE);
-    wlParameters.setText(BaseMessages.getString(PKG, "RestDialog.Parameters.Label"));
+    wlParameters = new Label(wParametersComp, SWT.LEFT | SWT.WRAP);
+    wlParameters.setText(BaseMessages.getString(PKG, "RestDialog.Parameters.Info"));
     PropsUi.setLook(wlParameters);
     FormData fdlParameters = new FormData();
     fdlParameters.left = new FormAttachment(0, 0);
+    fdlParameters.right = new FormAttachment(100, -margin);
     fdlParameters.top = new FormAttachment(wSpacer, margin);
     wlParameters.setLayoutData(fdlParameters);
 
@@ -762,7 +764,7 @@ public class RestDialog extends BaseTransformDialog {
     queryParameterColumns =
         new ColumnInfo[] {
           new ColumnInfo(
-              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterField"),
+              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.ParameterValueField"),
               ColumnInfo.COLUMN_TYPE_CCOMBO,
               new String[] {""},
               false),
@@ -817,12 +819,12 @@ public class RestDialog extends BaseTransformDialog {
     colinf =
         new ColumnInfo[] {
           new ColumnInfo(
-              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.Field"),
+              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.HeaderValueField"),
               ColumnInfo.COLUMN_TYPE_CCOMBO,
               new String[] {""},
               false),
           new ColumnInfo(
-              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.Name"),
+              BaseMessages.getString(PKG, "RestDialog.ColumnInfo.HeaderName"),
               ColumnInfo.COLUMN_TYPE_TEXT,
               false)
         };
diff --git a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
index 40edaee08f..25153e17db 100644
--- a/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
+++ b/plugins/transforms/rest/src/main/resources/org/apache/hop/pipeline/transforms/rest/messages/messages_en_US.properties
@@ -43,6 +43,13 @@ Rest.Log.NoField=URL field name is missing\!
 Rest.Log.queryParameterValue=Query parameter [{0}]\=''{1}''
 Rest.Log.ResponseCode=The response code is {0}
 Rest.Log.ResponseTime=Response time (milliseconds)\: [{0}] for [{1}]
+Rest.Log.ResponseBody=Response body [{0}]
+Rest.Log.ResponseError=REST [{0}] request to [{1}] returned HTTP status [{2}]
+Rest.Log.SendingRequest=Sending [{0}] request to [{1}]
+Rest.Log.Timeouts=Applied connect timeout [{0}] ms and read timeout [{1}] ms (-1 \= not set, engine default is used)
+Rest.Log.UsingConnection=Using REST connection [{0}] with base URL [{1}]
+Rest.Log.NoConnection=No REST connection configured, connecting to the URL directly
+Rest.Log.RequestContentType=Request Content-Type [{0}]
 Rest.Log.PagingEmptyJsonArrayFirstPage=REST paging: HTTP success with an empty JSON array ([]); no rows to emit.
 Rest.Log.PagingEmptyCollectionFirstPage=REST paging: HTTP success with an empty collection at split path "{0}"; no rows to emit.
 Rest.Log.LinkPaginationStoppedRepeatedUrl=REST Link header pagination stopped: the next request URL was already fetched ({0}), avoiding an infinite loop when rel=\"next\" repeats or cycles.
@@ -54,10 +61,12 @@ Rest.Name=REST client
 RestDialog.ApplicationType.Label=Application type
 RestDialog.Auth.Title=Authentication
 RestDialog.Body.Label=Body field
-RestDialog.ColumnInfo.Field=Field
-RestDialog.ColumnInfo.Name=Name
-RestDialog.ColumnInfo.ParameterField=Parameter field
+RestDialog.ColumnInfo.HeaderName=Header name
+RestDialog.ColumnInfo.HeaderValueField=Header value field
+RestDialog.ColumnInfo.MatrixParameterName=Matrix parameter name
+RestDialog.ColumnInfo.MatrixParameterValueField=Matrix parameter value field
 RestDialog.ColumnInfo.ParameterName=Parameter name
+RestDialog.ColumnInfo.ParameterValueField=Parameter value field
 RestDialog.ConnectionTimeout.Label=Connection timeout (milliseconds)
 RestDialog.ConnectionTimeout.Tooltip=The timeout until a connection is established (milliseconds)
 RestDialog.FailedToGetFields.DialogMessage=Error getting fields from incoming stream
@@ -77,13 +86,14 @@ RestDialog.HttpPassword.Tooltip=HTTP password used to authenticate the call
 RestDialog.IgnoreSsl.Label=Ignore SSL certificate check
 RestDialog.Log.FoundArguments=Found {0} arguments
 RestDialog.Log.GettingKeyInfo=getting key info...
+RestDialog.MatrixParameters.Info=Matrix parameters are added to the URL path, e.g. https://host/path;name1\=value1;name2\=value2
 RestDialog.MatrixParameters.Title=Matrix Parameters
 RestDialog.Method.Label=HTTP method
 RestDialog.MethodField.Label=Method field name
 RestDialog.MethodInField.Label=Get Method from field
 RestDialog.OutputFieldsGroup.Label=Output fields
-RestDialog.Parameters.Label=Parameters
-RestDialog.Parameters.Title=Parameters
+RestDialog.Parameters.Info=Query parameters are added to the end of the URL, e.g. https://host/path?name1\=value1&name2\=value2
+RestDialog.Parameters.Title=Query Parameters
 RestDialog.Preemptive.Label=Use preemptive authentication
 RestDialog.ProxyGroup.Label=Proxy to use
 RestDialog.ProxyHost.Label=Proxy Host
diff --git a/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestCallRestTest.java b/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestCallRestTest.java
index 7552e6d608..1cf15c9b19 100644
--- a/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestCallRestTest.java
+++ b/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestCallRestTest.java
@@ -67,6 +67,7 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
@@ -259,6 +260,134 @@ class RestCallRestTest {
     }
   }
 
+  @Test
+  void testCallRestPostWithoutBodySendsEmptyEntity() throws HopException {
+    // Issue #7621: a body-less POST must still produce a Content-Length header. The transform
+    // normalizes a null body to an empty string so the JDK connector used by the REST-connection
+    // path sends Content-Length: 0 instead of omitting it. Here we assert the transform posts a
+    // non-null, empty entity rather than a null one.
+    Response response = mock(Response.class);
+    when(response.getStatus()).thenReturn(200);
+    when(response.readEntity(String.class)).thenReturn("");
+    when(response.getHeaders()).thenReturn(new MultivaluedHashMap<>());
+
+    Invocation.Builder builder = mock(Invocation.Builder.class);
+    when(builder.post(any(Entity.class))).thenReturn(response);
+    when(builder.header(anyString(), any())).thenReturn(builder);
+    when(builder.accept((MediaType[]) any())).thenReturn(builder);
+
+    WebTarget webTarget = mock(WebTarget.class);
+    when(webTarget.request()).thenReturn(builder);
+    when(webTarget.getUri()).thenReturn(URI.create("http://example.com/api"));
+
+    Client client = mock(Client.class);
+    when(client.target(anyString())).thenReturn(webTarget);
+
+    ClientBuilder clientBuilder = mock(ClientBuilder.class);
+    when(clientBuilder.withConfig(any(ClientConfig.class))).thenReturn(clientBuilder);
+    when(clientBuilder.property(anyString(), any())).thenReturn(clientBuilder);
+    when(clientBuilder.hostnameVerifier(any())).thenReturn(clientBuilder);
+    when(clientBuilder.sslContext(any())).thenReturn(clientBuilder);
+    when(clientBuilder.build()).thenReturn(client);
+
+    try (MockedStatic<ClientBuilder> mockedStatic = Mockito.mockStatic(ClientBuilder.class)) {
+      mockedStatic.when(ClientBuilder::newBuilder).thenReturn(clientBuilder);
+
+      TransformMeta transformMeta = new TransformMeta();
+      transformMeta.setName("TestRest");
+      PipelineMeta pipelineMeta = new PipelineMeta();
+      pipelineMeta.setName("TestRest");
+      pipelineMeta.addTransform(transformMeta);
+
+      RestMeta meta = new RestMeta();
+      meta.setMethod(RestMeta.HTTP_METHOD_POST);
+      meta.setUrl("http://example.com/api");
+      meta.setResultField(new ResultField());
+      meta.getResultField().setFieldName("result");
+
+      RestData data = new RestData();
+      data.config = new ClientConfig();
+      data.mediaType = MediaType.APPLICATION_JSON_TYPE;
+      data.method = RestMeta.HTTP_METHOD_POST;
+      data.realUrl = "http://example.com/api";
+      data.resultFieldName = "result";
+      data.useBody = false; // no body configured -> entityString is null
+
+      IRowMeta inputRowMeta = new RowMeta();
+      inputRowMeta.addValueMeta(new ValueMetaString("field1"));
+      data.inputRowMeta = inputRowMeta;
+
+      Rest rest =
+          spy(new Rest(transformMeta, meta, data, 0, pipelineMeta, spy(new LocalPipelineEngine())));
+      when(rest.createClientBuilder()).thenReturn(clientBuilder);
+      rest.setMetadataProvider(mock(IHopMetadataProvider.class));
+
+      Object[] outputRow = rest.callRest(new Object[] {"value1"});
+
+      assertNotNull(outputRow);
+      ArgumentCaptor<Entity> entityCaptor = ArgumentCaptor.forClass(Entity.class);
+      verify(builder, times(1)).post(entityCaptor.capture());
+      assertEquals("", entityCaptor.getValue().getEntity());
+    }
+  }
+
+  @Test
+  void testAppendMatrixAndQueryParamsBakesParamsIntoUrl() throws HopException {
+    // Issue #7621: the REST-connection path builds its target inside RestConnection, so the
+    // transform bakes the configured matrix/query parameters into the URL to stay equivalent to the
+    // standalone path. This verifies both parameter kinds end up on the resulting URL.
+    RestData data = new RestData();
+    data.useParams = true;
+    data.nrParams = 2;
+    data.paramNames = new String[] {"q", "lang"};
+    data.indexOfParamFields = new int[] {0, 1};
+    data.useMatrixParams = true;
+    data.nrMatrixParams = 1;
+    data.matrixParamNames = new String[] {"author"};
+    data.indexOfMatrixParamFields = new int[] {2};
+
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaString("qField"));
+    inputRowMeta.addValueMeta(new ValueMetaString("langField"));
+    inputRowMeta.addValueMeta(new ValueMetaString("authorField"));
+    data.inputRowMeta = inputRowMeta;
+
+    TransformMeta transformMeta = new TransformMeta();
+    transformMeta.setName("TestRest");
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("TestRest");
+    pipelineMeta.addTransform(transformMeta);
+
+    Rest rest =
+        new Rest(transformMeta, new RestMeta(), data, 0, pipelineMeta, new LocalPipelineEngine());
+
+    Object[] row = new Object[] {"search", "en", "shakespeare"};
+    String url = rest.appendMatrixAndQueryParams("http://example.com/api", row);
+
+    assertTrue(url.contains("q=search"), url);
+    assertTrue(url.contains("lang=en"), url);
+    assertTrue(url.contains("author=shakespeare"), url);
+  }
+
+  @Test
+  void testAppendMatrixAndQueryParamsNoParamsReturnsUrlUnchanged() throws HopException {
+    RestData data = new RestData();
+    data.useParams = false;
+    data.useMatrixParams = false;
+
+    TransformMeta transformMeta = new TransformMeta();
+    transformMeta.setName("TestRest");
+    PipelineMeta pipelineMeta = new PipelineMeta();
+    pipelineMeta.setName("TestRest");
+    pipelineMeta.addTransform(transformMeta);
+
+    Rest rest =
+        new Rest(transformMeta, new RestMeta(), data, 0, pipelineMeta, new LocalPipelineEngine());
+
+    String url = "http://example.com/api?existing=1";
+    assertEquals(url, rest.appendMatrixAndQueryParams(url, new Object[] {}));
+  }
+
   @Test
   void testCallRestWithPutMethod() throws HopException {
     // Setup mocks