This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 7aa99cd424 fix(admin): preserve long API debug request headers (#7892)
7aa99cd424 is described below
commit 7aa99cd4247652f86c3302934769b433eac86af2
Author: cloudbase-ai <[email protected]>
AuthorDate: Thu Oct 22 10:56:50 2026 +0800
fix(admin): preserve long API debug request headers (#7892)
Signed-off-by: cloudbase-ai <[email protected]>
Co-authored-by: cloudbase-ai
<[email protected]>
---
.github/scripts/native-sql-matrix.py | 23 ++++
.github/scripts/test-native-sql-matrix.py | 13 ++
.github/workflows/native-sql-matrix.yml | 2 +-
db/init/mysql/schema.sql | 2 +-
db/init/oracle/schema.sql | 2 +-
db/init/pg/create-table.sql | 2 +-
db/upgrade/2.7.2-upgrade-2.7.3-h2.sql | 3 +
db/upgrade/2.7.2-upgrade-2.7.3-mysql.sql | 4 +
db/upgrade/2.7.2-upgrade-2.7.3-ob.sql | 4 +
db/upgrade/2.7.2-upgrade-2.7.3-og.sql | 4 +
db/upgrade/2.7.2-upgrade-2.7.3-oracle.sql | 9 ++
db/upgrade/2.7.2-upgrade-2.7.3-pg.sql | 4 +
db/upgrade/native-sql-matrix.md | 7 ++
db/upgrade/upgrade-guide.md | 24 ++++
.../oracle/OracleClobStringTypeHandler.java | 54 ++++++++
.../oracle/OracleSQLPrepareInterceptor.java | 5 +-
.../mappers/mock-request-record-sqlmap.xml | 66 +++++++---
.../src/main/resources/sql-script/h2/schema.sql | 2 +-
.../admin/mapper/InstanceInfoUpsertTest.java | 1 +
.../mapper/MockRequestHeaderMigrationTest.java | 108 ++++++++++++++++
.../mapper/MockRequestHeaderPersistenceTest.java | 137 +++++++++++++++++++++
.../oracle/OracleSQLPrepareInterceptorTest.java | 27 ++++
22 files changed, 482 insertions(+), 21 deletions(-)
diff --git a/.github/scripts/native-sql-matrix.py
b/.github/scripts/native-sql-matrix.py
index 570cc7b00a..3e01ac696f 100644
--- a/.github/scripts/native-sql-matrix.py
+++ b/.github/scripts/native-sql-matrix.py
@@ -400,6 +400,26 @@ class Engine:
else:
raise AssertionError("Duplicate discovery upstream was accepted")
+ def insert_legacy_mock_header(self):
+ self.sql("INSERT INTO mock_request_record
(id,api_id,host,port,url,path_variable,query,header,body) "
+ "VALUES
('sql-header-legacy','sql-header-api','localhost',9195,'http://localhost/test','path','query',"
+ "'{\"Authorization\":\"legacy-token\"}','legacy-body');")
+
+ def check_mock_headers(self, upgrade=False):
+ if upgrade:
+ combined = "header||'|'||body" if self.dialect in ("pg", "og",
"oracle") else "CONCAT(header,'|',body)"
+ if scalar(self.sql("SELECT " + combined + " FROM
mock_request_record WHERE id='sql-header-legacy';")) != (
+ '{"Authorization":"legacy-token"}|legacy-body'):
+ raise AssertionError("Header migration changed the existing
header/body or row identity")
+ if self.dialect == "oracle":
+ value = "TO_CLOB(RPAD('t',3000,'t'))||TO_CLOB(RPAD('t',3000,'t'))"
+ else:
+ value = "REPEAT('t',6000)"
+ self.sql("INSERT INTO mock_request_record
(id,api_id,host,port,url,path_variable,query,header) "
+ "VALUES
('sql-header-long','sql-header-api','localhost',9195,'http://localhost/test','path','query',"
+ value + ");")
+ if scalar(self.sql("SELECT LENGTH(header) FROM mock_request_record
WHERE id='sql-header-long';")) != "6000":
+ raise AssertionError("Long request header was truncated")
+
def insert_data_permissions(self, duplicates=False):
grants = [("1", "sql-matrix-user", "sql-matrix-data", 0),
("4", "sql-matrix-user-other", "sql-matrix-data", 0),
@@ -531,6 +551,7 @@ def execute_matrix(dialect, root, output):
before = {table: set(engine.sql("SELECT id FROM " +
engine.table(table) + ";").split()) for table in TABLES}
if flow == "upgrade":
engine.insert_data_permissions(duplicates=True)
+ engine.insert_legacy_mock_header()
for step in MIGRATIONS_FROM_BASELINE:
if step == "2.7.2-upgrade-2.7.3":
engine.insert_instance_observations(duplicates=True)
@@ -550,6 +571,8 @@ def execute_matrix(dialect, root, output):
indexes = engine.indexes()
engine.check_data_permission_uniqueness(indexes)
engine.check_instance_identity(indexes, root /
"shenyu-admin/src/main/resources/mappers/instance-info-sqlmap.xml")
+
+ engine.check_mock_headers(upgrade=flow == "upgrade")
if flow == "fresh" and dialect in ("mysql", "ob"):
engine.check_fresh_mysql_schema(schema, indexes)
if flow == "fresh" and dialect in ("pg", "og"):
diff --git a/.github/scripts/test-native-sql-matrix.py
b/.github/scripts/test-native-sql-matrix.py
index e9b1263569..f3fd6ebc8c 100644
--- a/.github/scripts/test-native-sql-matrix.py
+++ b/.github/scripts/test-native-sql-matrix.py
@@ -289,6 +289,19 @@ class NativeSqlMatrixTest(unittest.TestCase):
with self.assertRaises(RuntimeError if mode ==
"unrelated-error" else AssertionError):
engine.check_instance_identity(indexes, mapper)
+ def test_mock_header_checks_reject_truncation_and_legacy_value_loss(self):
+ for dialect in matrix.DIALECTS:
+ for preserved, length in ((True, "6000"), (False, "6000"), (True,
"1024")):
+ with self.subTest(dialect=dialect, preserved=preserved,
length=length), tempfile.TemporaryDirectory() as directory:
+ engine = matrix.Engine(dialect, "upgrade", Path(directory))
+ values = ['{"Authorization":"legacy-token"}|legacy-body'
if preserved else "changed", "", length]
+ with patch.object(engine, "sql", side_effect=values):
+ if preserved and length == "6000":
+ engine.check_mock_headers(upgrade=True)
+ else:
+ with self.assertRaises(AssertionError):
+ engine.check_mock_headers(upgrade=True)
+
def test_actual_mapper_projects_metadata_and_preserves_detail_jar(self):
listing, detail = matrix.fragment_columns(ROOT /
"shenyu-admin/src/main/resources/mappers/plugin-sqlmap.xml")
self.assertNotIn("plugin_jar", listing)
diff --git a/.github/workflows/native-sql-matrix.yml
b/.github/workflows/native-sql-matrix.yml
index 7c97f8797e..838420f5a0 100644
--- a/.github/workflows/native-sql-matrix.yml
+++ b/.github/workflows/native-sql-matrix.yml
@@ -77,7 +77,7 @@ jobs:
- name: Verify list/detail/export and index contracts
run: >-
exec bash .github/scripts/maven-with-retry.sh ./mvnw -B -ntp -pl
shenyu-admin -am test
-
-Dtest=PluginMapperTest,PluginServiceTest,PluginControllerTest,AdminQueryIndexTest,PluginIndexUpgradeContractTest,DataPermissionMapperTest,PermissionListenerIntegrationTest,InstanceInfoUpsertTest
+
-Dtest=PluginMapperTest,PluginServiceTest,PluginControllerTest,AdminQueryIndexTest,PluginIndexUpgradeContractTest,DataPermissionMapperTest,PermissionListenerIntegrationTest,InstanceInfoUpsertTest,MockRequestHeaderPersistenceTest,MockRequestHeaderMigrationTest,OracleSQLPrepareInterceptorTest
-Dsurefire.failIfNoSpecifiedTests=false -Dmaven.javadoc.skip=true
native-sql:
diff --git a/db/init/mysql/schema.sql b/db/init/mysql/schema.sql
index b3c78a36ad..c4ad22ff91 100644
--- a/db/init/mysql/schema.sql
+++ b/db/init/mysql/schema.sql
@@ -263,7 +263,7 @@ CREATE TABLE `mock_request_record` (
`url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT
NULL COMMENT 'the request url',
`path_variable` varchar(255) CHARACTER SET utf8mb4 COLLATE
utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'the request param in url',
`query` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT
NULL DEFAULT '' COMMENT 'the request param after url',
- `header` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
NOT NULL DEFAULT '' COMMENT 'the request param in header',
+ `header` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
COMMENT 'the request param in header',
`body` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'the
request body',
`date_created` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT
'create time',
`date_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON
UPDATE CURRENT_TIMESTAMP(3) COMMENT 'update time',
diff --git a/db/init/oracle/schema.sql b/db/init/oracle/schema.sql
index 543a0a0cc9..b660cc83e9 100644
--- a/db/init/oracle/schema.sql
+++ b/db/init/oracle/schema.sql
@@ -317,7 +317,7 @@ create table mock_request_record
url VARCHAR2(1024) not null,
path_variable VARCHAR2(255) default '' not null,
query VARCHAR2(1024) default '' not null,
- header VARCHAR2(1024) default '' not null,
+ header CLOB default EMPTY_CLOB() not null,
body CLOB,
date_created timestamp(3) default SYSDATE not null,
date_updated timestamp(3) default SYSDATE not null
diff --git a/db/init/pg/create-table.sql b/db/init/pg/create-table.sql
index 7f6179e235..076e91c2f3 100644
--- a/db/init/pg/create-table.sql
+++ b/db/init/pg/create-table.sql
@@ -321,7 +321,7 @@ CREATE TABLE "public"."mock_request_record" (
"url" varchar(1024) COLLATE "pg_catalog"."default" NOT NULL,
"path_variable" varchar(255) COLLATE "pg_catalog"."default" NOT NULL DEFAULT
'',
"query" varchar(1024) COLLATE "pg_catalog"."default" NOT NULL DEFAULT '',
- "header" varchar(1024) COLLATE "pg_catalog"."default" NOT NULL DEFAULT '',
+ "header" text COLLATE "pg_catalog"."default" NOT NULL DEFAULT '',
"body" text COLLATE "pg_catalog"."default",
"date_created" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text,
(now())::timestamp(0) without time zone),
"date_updated" timestamp(6) NOT NULL DEFAULT timezone('UTC-8'::text,
(now())::timestamp(0) without time zone)
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-h2.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-h2.sql
index 541a12baf7..93b6e45b59 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-h2.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-h2.sql
@@ -40,3 +40,6 @@ WHERE EXISTS (
AND retained_instance.id > duplicate_instance.id))
);
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers.
+ALTER TABLE mock_request_record ALTER COLUMN header CLOB;
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-mysql.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-mysql.sql
index 097c3c8f83..c550610917 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-mysql.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-mysql.sql
@@ -38,3 +38,7 @@ JOIN instance_info retained_instance ON
AND retained_instance.date_created =
duplicate_instance.date_created
AND retained_instance.id > duplicate_instance.id));
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers, including long Authorization
values.
+-- Stop Admin writers and back up mock_request_record before the one-time
migration.
+ALTER TABLE mock_request_record MODIFY COLUMN header TEXT CHARACTER SET
utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'the request param in
header';
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-ob.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-ob.sql
index 06090fd91d..f9959945f7 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-ob.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-ob.sql
@@ -38,3 +38,7 @@ JOIN instance_info retained_instance ON
AND retained_instance.date_created =
duplicate_instance.date_created
AND retained_instance.id > duplicate_instance.id));
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers, including long Authorization
values.
+-- Stop Admin writers and back up mock_request_record before the one-time
migration.
+ALTER TABLE mock_request_record MODIFY COLUMN header TEXT CHARACTER SET
utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'the request param in
header';
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-og.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-og.sql
index 58657c681e..382cad4c5a 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-og.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-og.sql
@@ -40,3 +40,7 @@ WHERE EXISTS (
AND retained_instance.id > duplicate_instance.id))
);
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers, including long Authorization
values.
+-- Stop Admin writers and back up mock_request_record before the one-time
migration.
+ALTER TABLE public.mock_request_record ALTER COLUMN header TYPE TEXT;
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-oracle.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-oracle.sql
index b730778015..f51c04e9d6 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-oracle.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-oracle.sql
@@ -40,3 +40,12 @@ WHERE EXISTS (
AND retained_instance.id > duplicate_instance.id))
);
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers, including long Authorization
values.
+-- Stop Admin writers and back up mock_request_record before the one-time
migration.
+ALTER TABLE mock_request_record ADD header_clob CLOB;
+UPDATE mock_request_record SET header_clob = COALESCE(TO_CLOB(header),
EMPTY_CLOB());
+ALTER TABLE mock_request_record DROP COLUMN header;
+ALTER TABLE mock_request_record RENAME COLUMN header_clob TO header;
+ALTER TABLE mock_request_record MODIFY header DEFAULT EMPTY_CLOB() NOT NULL;
+COMMENT ON COLUMN mock_request_record.header IS 'the request param in header';
diff --git a/db/upgrade/2.7.2-upgrade-2.7.3-pg.sql
b/db/upgrade/2.7.2-upgrade-2.7.3-pg.sql
index 375740e936..ca3a38ac34 100644
--- a/db/upgrade/2.7.2-upgrade-2.7.3-pg.sql
+++ b/db/upgrade/2.7.2-upgrade-2.7.3-pg.sql
@@ -40,3 +40,7 @@ WHERE EXISTS (
AND retained_instance.id > duplicate_instance.id))
);
CREATE UNIQUE INDEX uk_instance_info_identity ON instance_info (namespace_id,
instance_ip, instance_port, instance_type);
+
+-- Preserve complete API-debug request headers, including long Authorization
values.
+-- Stop Admin writers and back up mock_request_record before the one-time
migration.
+ALTER TABLE public.mock_request_record ALTER COLUMN header TYPE TEXT;
diff --git a/db/upgrade/native-sql-matrix.md b/db/upgrade/native-sql-matrix.md
index c69c789aa7..632864a6c3 100644
--- a/db/upgrade/native-sql-matrix.md
+++ b/db/upgrade/native-sql-matrix.md
@@ -91,6 +91,13 @@ update SQL with stale, zero, newer and equal heartbeat
timestamps. The H2 servic
suite additionally forces two independent writers to observe absence before
either insert, in both insertion orders, and verifies caller-transaction
suspension.
+Mock-request header checks preserve a legacy header/body row across the
complete
+upgrade chain and insert a 6,000-character header in both flows. The H2 mapper
+suite covers API create/update, selective/batch inserts, 60,000-character
equality
+filters, Unicode and absent-header defaults; a separate H2 migration test
verifies
+existing rows survive the documented type alteration. Oracle expression
handling
+has dedicated interceptor regressions.
+
## Local execution
With a Linux Docker daemon available, from the repository root:
diff --git a/db/upgrade/upgrade-guide.md b/db/upgrade/upgrade-guide.md
index 9e7e1f2cad..dd3b87bfd6 100644
--- a/db/upgrade/upgrade-guide.md
+++ b/db/upgrade/upgrade-guide.md
@@ -4,6 +4,30 @@
## To Shenyu Users
+### API-debug request headers in 2.7.3
+
+Stop Admin writers and back up `mock_request_record` before applying the
+`2.7.2-upgrade-2.7.3-<dialect>.sql` migration. It changes `header` from
+`VARCHAR(1024)` to TEXT (MySQL/OceanBase/PostgreSQL/openGauss) or CLOB
(Oracle),
+preserving existing rows and header contents. Oracle copies the value to a CLOB
+column before replacing the old column because a direct VARCHAR2-to-CLOB type
+change is not supported. Run the complete version migration once, then deploy
+the updated Admin.
+
+The `2.7.2-upgrade-2.7.3-h2.sql` migration includes the persistent-H2 change
+(fresh H2 databases create CLOB directly). The header-specific statement is:
+
+```sql
+ALTER TABLE mock_request_record ALTER COLUMN header CLOB;
+```
+
+All Admin mapper insert paths store an absent header as an empty value. MySQL
5.7
+does not allow a literal default for TEXT, so the column has no default and
Admin
+normalizes the value explicitly. Custom SQL writers must supply a header
value. MySQL/OceanBase TEXT retains the
+native 65,535-byte limit. Oracle reads/writes use complete CLOB values and
equality
+filters compare the whole value, including content beyond the first 4,000
characters.
+
+
### `loged` column renamed to `logged` in 2.7.3
Running the `2.7.2-upgrade-2.7.3-<dialect>.sql` script is mandatory for every
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleClobStringTypeHandler.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleClobStringTypeHandler.java
new file mode 100644
index 0000000000..4ec7fc3f6d
--- /dev/null
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleClobStringTypeHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shenyu.admin.mybatis.oracle;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import java.io.StringReader;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Binds strings as JDBC CLOBs so Oracle SQL expressions receive LOBs rather
than CHAR parameters.
+ */
+public final class OracleClobStringTypeHandler extends BaseTypeHandler<String>
{
+
+ @Override
+ public void setNonNullParameter(final PreparedStatement statement, final
int index, final String value,
+ final JdbcType jdbcType) throws
SQLException {
+ statement.setClob(index, new StringReader(value));
+ }
+
+ @Override
+ public String getNullableResult(final ResultSet resultSet, final String
column) throws SQLException {
+ return resultSet.getString(column);
+ }
+
+ @Override
+ public String getNullableResult(final ResultSet resultSet, final int
column) throws SQLException {
+ return resultSet.getString(column);
+ }
+
+ @Override
+ public String getNullableResult(final CallableStatement statement, final
int column) throws SQLException {
+ return statement.getString(column);
+ }
+}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptor.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptor.java
index a5e9a8dfb6..ebd30de195 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptor.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptor.java
@@ -28,6 +28,7 @@ import org.apache.ibatis.plugin.Signature;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.Properties;
+import java.util.regex.Pattern;
/**
* the mybatis interceptor for update/insert/delete.
@@ -37,6 +38,8 @@ import java.util.Properties;
})
public class OracleSQLPrepareInterceptor implements Interceptor {
+ private static final Pattern MULTI_ROW_VALUES =
Pattern.compile("\\)\\s*,\\s*\\(");
+
@Override
public Object intercept(final Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler)
invocation.getTarget();
@@ -72,7 +75,7 @@ public class OracleSQLPrepareInterceptor implements
Interceptor {
.replace("update resource", "update \"resource\"");
}
// replace batch insert into
- if (replaceSql.contains("insert into") &&
replaceSql.split("\\(").length > 3) {
+ if (replaceSql.contains("insert into") &&
MULTI_ROW_VALUES.matcher(replaceSql).find()) {
replaceSql = replaceSql.replaceAll("\r|\n|\\s", "")
.replace("insertinto", "insert into ")
.replace("values", " SELECT * FROM (")
diff --git
a/shenyu-admin/src/main/resources/mappers/mock-request-record-sqlmap.xml
b/shenyu-admin/src/main/resources/mappers/mock-request-record-sqlmap.xml
index 85883814a1..dc6a311925 100644
--- a/shenyu-admin/src/main/resources/mappers/mock-request-record-sqlmap.xml
+++ b/shenyu-admin/src/main/resources/mappers/mock-request-record-sqlmap.xml
@@ -25,7 +25,7 @@
<result property="port" column="port" jdbcType="INTEGER"/>
<result property="pathVariable" column="path_variable"
jdbcType="VARCHAR"/>
<result property="query" column="query" jdbcType="VARCHAR"/>
- <result property="header" column="header" jdbcType="VARCHAR"/>
+ <result property="header" column="header" jdbcType="LONGVARCHAR"/>
<result property="body" column="body" jdbcType="VARCHAR"/>
<result property="dateCreated" column="date_created"
jdbcType="TIMESTAMP"/>
<result property="dateUpdated" column="date_updated"
jdbcType="TIMESTAMP"/>
@@ -35,6 +35,29 @@
id, api_id, host, port, url, path_variable, query, header, body,
date_created, date_updated
</sql>
+ <!-- headerProperty is a fixed mapper include property, never
request-supplied SQL. -->
+ <sql id="Header_Value">
+ <choose>
+ <when test="_databaseId == 'oracle'">
+ COALESCE(#{${headerProperty}, jdbcType=CLOB,
typeHandler=org.apache.shenyu.admin.mybatis.oracle.OracleClobStringTypeHandler},
EMPTY_CLOB())
+ </when>
+ <otherwise>
+ COALESCE(#{${headerProperty}, jdbcType=LONGVARCHAR}, '')
+ </otherwise>
+ </choose>
+ </sql>
+
+ <sql id="Header_Equals">
+ <choose>
+ <when test="_databaseId == 'oracle'">
+ DBMS_LOB.COMPARE(header, <include refid="Header_Value"/>) = 0
+ </when>
+ <otherwise>
+ header = <include refid="Header_Value"/>
+ </otherwise>
+ </choose>
+ </sql>
+
<select id="queryById" resultMap="MockRequestRecordMap">
select
<include refid="Base_Column_List"/>
@@ -61,7 +84,7 @@
and path_variable = #{pathVariable, jdbcType=VARCHAR}
</if>
<if test="header != null">
- and `header` = #{header, jdbcType=VARCHAR}
+ and <include refid="Header_Equals"><property
name="headerProperty" value="header"/></include>
</if>
</where>
</select>
@@ -93,10 +116,10 @@
and query = #{item.query}
</if>
<if test="item.header != null and item.header != ''">
- and header = #{item.header}
+ and <include refid="Header_Equals"><property
name="headerProperty" value="item.header"/></include>
</if>
<if test="item.body != null and item.body != ''">
- and body = #{item.body}
+ and body = #{item.body, jdbcType=VARCHAR}
</if>
<if test="item.dateCreated != null">
and date_created = #{item.dateCreated}
@@ -133,10 +156,10 @@
and query = #{query}
</if>
<if test="header != null and header != ''">
- and header = #{header}
+ and <include refid="Header_Equals"><property
name="headerProperty" value="header"/></include>
</if>
<if test="body != null and body != ''">
- and body = #{body}
+ and body = #{body, jdbcType=VARCHAR}
</if>
<if test="dateCreated != null">
and date_created = #{dateCreated}
@@ -161,7 +184,7 @@
AND ROWNUM = 1
</select>
- <insert id="insert" keyProperty="id" useGeneratedKeys="true">
+ <insert id="insert" keyColumn="id" keyProperty="id"
useGeneratedKeys="true">
insert into mock_request_record(
id,
api_id,
@@ -183,8 +206,8 @@
#{url},
#{pathVariable},
#{query},
- #{header},
- #{body},
+ <include refid="Header_Value"><property name="headerProperty"
value="header"/></include>,
+ #{body, jdbcType=VARCHAR},
#{dateCreated},
#{dateUpdated}
)
@@ -200,7 +223,7 @@
url,
<if test="pathVariable != null">path_variable,</if>
<if test="query != null">query,</if>
- <if test="header != null">header,</if>
+ header,
<if test="body != null">body,</if>
<if test="dateCreated != null">date_created,</if>
<if test="dateUpdated != null">date_updated,</if>
@@ -213,7 +236,7 @@
#{url,jdbcType=VARCHAR},
<if test="pathVariable !=
null">#{pathVariable,jdbcType=VARCHAR},</if>
<if test="query != null">#{query,jdbcType=VARCHAR},</if>
- <if test="header != null">#{header,jdbcType=VARCHAR},</if>
+ <include refid="Header_Value"><property name="headerProperty"
value="header"/></include>,
<if test="body != null">#{body,jdbcType=VARCHAR},</if>
<if test="dateCreated !=
null">#{dateCreated,jdbcType=TIMESTAMP},</if>
<if test="dateUpdated !=
null">#{dateUpdated,jdbcType=TIMESTAMP},</if>
@@ -244,14 +267,27 @@
#{item.url},
#{item.pathVariable},
#{item.query},
- #{item.header},
- #{item.body},
+ <include refid="Header_Value"><property name="headerProperty"
value="item.header"/></include>,
+ #{item.body, jdbcType=VARCHAR},
#{item.dateCreated},
#{item.dateUpdated}
)
</foreach>
</insert>
+ <!-- Keep CLOB expressions intact and retain application-assigned row IDs.
-->
+ <insert id="insertBatch" databaseId="oracle" useGeneratedKeys="false">
+ INSERT ALL
+ <foreach collection="list" item="item">
+ INTO mock_request_record (<include refid="Base_Column_List"/>)
+ VALUES (#{item.id}, #{item.apiId}, #{item.host}, #{item.port},
#{item.url},
+ #{item.pathVariable}, #{item.query},
+ <include refid="Header_Value"><property name="headerProperty"
value="item.header"/></include>,
+ #{item.body, jdbcType=VARCHAR}, #{item.dateCreated},
#{item.dateUpdated})
+ </foreach>
+ SELECT 1 FROM dual
+ </insert>
+
<update id="update">
update mock_request_record
<set>
@@ -274,10 +310,10 @@
query = #{query},
</if>
<if test="header != null and header != ''">
- header = #{header},
+ header = <include refid="Header_Value"><property
name="headerProperty" value="header"/></include>,
</if>
<if test="body != null and body != ''">
- body = #{body},
+ body = #{body, jdbcType=VARCHAR},
</if>
<if test="dateCreated != null">
date_created = #{dateCreated},
diff --git a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
index 13dc381e2d..860852f60a 100644
--- a/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
+++ b/shenyu-admin/src/main/resources/sql-script/h2/schema.sql
@@ -139,7 +139,7 @@ CREATE TABLE IF NOT EXISTS `mock_request_record` (
`url` varchar(1024) NOT NULL COMMENT 'the request url',
`path_variable` varchar(255) NOT NULL DEFAULT '' COMMENT 'the request param
in url',
`query` varchar(1024) NOT NULL DEFAULT '' COMMENT 'the request param after
url',
- `header` varchar(1024) NOT NULL DEFAULT '' COMMENT 'the request param in
header',
+ `header` clob NOT NULL DEFAULT '' COMMENT 'the request param in header',
`body` text COMMENT 'the request body',
`date_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create
time',
`date_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP COMMENT 'update time',
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoUpsertTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoUpsertTest.java
index e001c9d568..96cc90609f 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoUpsertTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/InstanceInfoUpsertTest.java
@@ -205,6 +205,7 @@ public final class InstanceInfoUpsertTest {
jdbc.execute("ALTER TABLE instance_info DROP CONSTRAINT
uk_instance_info_identity");
jdbc.execute("CREATE TABLE selector (loged INT)");
jdbc.execute("CREATE TABLE rule (loged INT)");
+ jdbc.execute("CREATE TABLE mock_request_record (header
VARCHAR(1024))");
insertLegacy("old-heartbeat", "ns", 100, "2026-01-03 00:00:00");
insertLegacy("latest-heartbeat", "ns", 200, "2026-01-01 00:00:00");
insertLegacy("tie-a", "ns-tie", 0, "2026-01-01 00:00:00");
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderMigrationTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderMigrationTest.java
new file mode 100644
index 0000000000..369065871f
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderMigrationTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.shenyu.admin.mapper;
+
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.shenyu.admin.model.entity.MockRequestRecordDO;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.api.Test;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.util.Objects;
+import java.sql.Connection;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Exercises the documented persistent-H2 migration without resetting existing
rows.
+ */
+public final class MockRequestHeaderMigrationTest {
+
+ @Test
+ public void testMigrationPreservesLegacyDataAndAcceptsLongHeaders() throws
Exception {
+ JdbcDataSource dataSource = new JdbcDataSource();
+ dataSource.setURL("jdbc:h2:mem:header-migration;MODE=MySQL");
+ try (Connection connection = dataSource.getConnection(); Statement
statement = connection.createStatement()) {
+ String schema;
+ try (InputStream in =
Resources.getResourceAsStream("sql-script/h2/schema.sql")) {
+ schema = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ Matcher table = Pattern.compile("(?is)CREATE TABLE IF NOT EXISTS
`mock_request_record`.*?;").matcher(schema);
+ assertTrue(table.find());
+ String legacy = table.group().replace("`header` clob", "`header`
varchar(1024)");
+ assertTrue(legacy.contains("`header` varchar(1024)"));
+ statement.execute(legacy);
+ MockRequestRecordMapper mapper = mapper(dataSource);
+ Timestamp time = Timestamp.valueOf("2026-01-01 00:00:00");
+ MockRequestRecordDO record =
MockRequestRecordDO.builder().id("legacy").apiId("legacy-api")
+
.host("localhost").port(9195).url("http://localhost/test").pathVariable("path").query("query")
+
.header("{\"X-Legacy\":\"汉字\"}").body("legacy-body").dateCreated(time).dateUpdated(time).build();
+ mapper.insert(record);
+
+ statement.execute("CREATE TABLE selector (loged INT)");
+ statement.execute("CREATE TABLE rule (loged INT)");
+ statement.execute("CREATE TABLE instance_info (id VARCHAR(128),
namespace_id VARCHAR(50), instance_ip VARCHAR(128),"
+ + "instance_port VARCHAR(128), instance_type VARCHAR(128),
last_heart_beat_time BIGINT, date_updated TIMESTAMP, date_created TIMESTAMP)");
+ new ResourceDatabasePopulator(new
FileSystemResource(upgradeScript())).execute(dataSource);
+
+ MockRequestRecordDO restored = mapper.queryById("legacy");
+ assertEquals(record.getHeader(), restored.getHeader());
+ assertEquals(record.getBody(), restored.getBody());
+ assertEquals(time, restored.getDateCreated());
+ record.setHeader("{\"Authorization\":\"Bearer " +
"t".repeat(16384) + "\"}");
+ assertEquals(1, mapper.update(record));
+ assertEquals(record.getHeader(),
mapper.queryById("legacy").getHeader());
+ assertEquals("legacy-body", mapper.queryById("legacy").getBody());
+ }
+ }
+
+ private Path upgradeScript() {
+ Path root = Path.of(System.getProperty("user.dir")).toAbsolutePath();
+ while (Objects.nonNull(root) &&
!Files.isDirectory(root.resolve("db/upgrade"))) {
+ root = root.getParent();
+ }
+ org.junit.jupiter.api.Assertions.assertNotNull(root);
+ return root.resolve("db/upgrade/2.7.2-upgrade-2.7.3-h2.sql");
+ }
+
+ private MockRequestRecordMapper mapper(final JdbcDataSource dataSource)
throws Exception {
+ Configuration configuration = new Configuration(new
Environment("header-migration", new SpringManagedTransactionFactory(),
dataSource));
+ configuration.setDatabaseId("h2");
+ configuration.setMapUnderscoreToCamelCase(true);
+ try (InputStream in =
Resources.getResourceAsStream("mappers/mock-request-record-sqlmap.xml")) {
+ new XMLMapperBuilder(in, configuration,
"mappers/mock-request-record-sqlmap.xml",
configuration.getSqlFragments()).parse();
+ }
+ return new SqlSessionTemplate(new
SqlSessionFactoryBuilder().build(configuration)).getMapper(MockRequestRecordMapper.class);
+ }
+}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderPersistenceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderPersistenceTest.java
new file mode 100644
index 0000000000..95581ac25e
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mapper/MockRequestHeaderPersistenceTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.shenyu.admin.mapper;
+
+import jakarta.annotation.Resource;
+import org.apache.shenyu.admin.AbstractSpringIntegrationTest;
+import org.apache.shenyu.admin.model.dto.MockRequestRecordDTO;
+import org.apache.shenyu.admin.model.entity.MockRequestRecordDO;
+import org.apache.shenyu.admin.model.query.MockRequestRecordQuery;
+import org.apache.shenyu.admin.model.vo.MockRequestRecordVO;
+import org.apache.shenyu.admin.service.MockRequestRecordService;
+import org.apache.shenyu.common.utils.UUIDUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies complete header persistence and equality filtering with production
mappers.
+ */
+@Transactional
+public class MockRequestHeaderPersistenceTest extends
AbstractSpringIntegrationTest {
+
+ @Resource
+ private MockRequestRecordMapper mapper;
+
+ @Resource
+ private MockRequestRecordService service;
+
+ @ParameterizedTest
+ @ValueSource(ints = {2048, 16384, 60000})
+ public void testServiceSavesAndUpdatesLongHeaders(final int length) {
+ MockRequestRecordDO record = record(header(length));
+ MockRequestRecordDTO request = new MockRequestRecordDTO();
+ request.setApiId(record.getApiId());
+ request.setHost(record.getHost());
+ request.setPort(record.getPort());
+ request.setUrl(record.getUrl());
+ request.setPathVariable(record.getPathVariable());
+ request.setQuery(record.getQuery());
+ request.setHeader(record.getHeader());
+ assertEquals(1, service.createOrUpdate(request));
+ MockRequestRecordVO saved = service.queryByApiId(record.getApiId());
+ assertEquals(record.getHeader(), saved.getHeader());
+ request.setId(saved.getId());
+ request.setHeader(header(length) + " ");
+ assertEquals(1, service.createOrUpdate(request));
+ assertEquals(request.getHeader(),
service.findById(saved.getId()).getHeader());
+ }
+
+ @Test
+ public void testSelectiveAndBatchWritesKeepCompleteHeaders() {
+ MockRequestRecordDO selective = record(header(16384));
+ assertEquals(1, mapper.insertSelective(selective));
+ assertEquals(selective.getHeader(),
mapper.queryById(selective.getId()).getHeader());
+ MockRequestRecordDO first = record(header(32768));
+ MockRequestRecordDO second = record(header(16384));
+ mapper.insertBatch(List.of(first, second));
+ assertEquals(first.getHeader(),
mapper.queryById(first.getId()).getHeader());
+ assertEquals(second.getHeader(),
mapper.queryById(second.getId()).getHeader());
+ }
+
+ @Test
+ public void testLongHeaderEqualityFiltersCompareTheWholeValue() {
+ MockRequestRecordDO record = record(header(60000));
+ assertEquals(1, mapper.insert(record));
+ MockRequestRecordQuery query = new MockRequestRecordQuery();
+ query.setApiId(record.getApiId());
+ query.setHeader(record.getHeader());
+ assertEquals(1, mapper.selectByQuery(query).size());
+ MockRequestRecordDO filter = new MockRequestRecordDO();
+ filter.setId(record.getId());
+ filter.setHeader(record.getHeader());
+ assertEquals(1, mapper.queryAll(filter).size());
+ assertEquals(1, mapper.count(filter));
+
+ // Keep the long common prefix so truncated comparisons cannot pass.
+ query.setHeader(record.getHeader() + "different");
+ filter.setHeader(query.getHeader());
+ assertTrue(mapper.selectByQuery(query).isEmpty());
+ assertTrue(mapper.queryAll(filter).isEmpty());
+ assertEquals(0, mapper.count(filter));
+ }
+
+ @Test
+ public void testMissingHeadersRemainEmpty() {
+ MockRequestRecordDO full = record(null);
+ mapper.insert(full);
+ assertEquals("", mapper.queryById(full.getId()).getHeader());
+ MockRequestRecordDO selective = record(null);
+ mapper.insertSelective(selective);
+ assertEquals("", mapper.queryById(selective.getId()).getHeader());
+ MockRequestRecordDO batch = record(null);
+ mapper.insertBatch(List.of(batch));
+ assertEquals("", mapper.queryById(batch.getId()).getHeader());
+ }
+
+ @Test
+ public void testUnicodeHeaderRoundTrip() {
+ MockRequestRecordDO record = record("{\"X-Note\":\"" +
"汉字🙂".repeat(2000) + "\"}");
+ mapper.insert(record);
+ assertEquals(record.getHeader(),
mapper.queryById(record.getId()).getHeader());
+ }
+
+ private String header(final int length) {
+ return "{\"Authorization\":\"Bearer " + "t".repeat(length) + "\"}";
+ }
+
+ private MockRequestRecordDO record(final String header) {
+ String id = UUIDUtils.getInstance().generateShortUuid();
+ Timestamp time = Timestamp.valueOf("2026-01-01 00:00:00");
+ return MockRequestRecordDO.builder().id(id).apiId("header-" +
id).host("localhost").port(9195)
+
.url("http://localhost:9195/test").pathVariable("path").query("query").header(header)
+ .dateCreated(time).dateUpdated(time).build();
+ }
+}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptorTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptorTest.java
index 6c011946ae..8811edec4a 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptorTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/mybatis/oracle/OracleSQLPrepareInterceptorTest.java
@@ -20,10 +20,13 @@ package org.apache.shenyu.admin.mybatis.oracle;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.session.Configuration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Properties;
+import java.util.Collections;
+import java.util.Locale;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -51,6 +54,30 @@ public class OracleSQLPrepareInterceptorTest {
Assertions.assertDoesNotThrow(() ->
oracleSQLPrepareInterceptor.intercept(invocation));
}
+ @Test
+ public void testSingleInsertWithClobExpressionsIsNotRewrittenAsABatch() {
+ String sql = "INSERT INTO mock_request_record (id, header) VALUES (?,
COALESCE(?, EMPTY_CLOB()))";
+ BoundSql boundSql = prepare(sql);
+ Assertions.assertEquals(sql.toLowerCase(Locale.ROOT),
boundSql.getSql());
+ }
+
+ @Test
+ public void testMultiRowInsertStillUsesOracleSelectSyntax() {
+ BoundSql boundSql = prepare("INSERT INTO example (id, value) VALUES
(?, ?), (?, ?)");
+ Assertions.assertTrue(boundSql.getSql().contains("UNION ALL"));
+ Assertions.assertTrue(boundSql.getSql().contains("FROM dual"));
+ }
+
+ private BoundSql prepare(final String sql) {
+ BoundSql boundSql = new BoundSql(new Configuration(), sql,
Collections.emptyList(), null);
+ Invocation invocation = mock(Invocation.class);
+ StatementHandler handler = mock(StatementHandler.class);
+ when(invocation.getTarget()).thenReturn(handler);
+ when(handler.getBoundSql()).thenReturn(boundSql);
+ Assertions.assertDoesNotThrow(() -> new
OracleSQLPrepareInterceptor().intercept(invocation));
+ return boundSql;
+ }
+
@Test
public void pluginTest() {
final OracleSQLPrepareInterceptor oracleSQLPrepareInterceptor = new
OracleSQLPrepareInterceptor();