Skip to content

Commit 5ab3688

Browse files
committed
fix(core): strip null bytes from JSONB values before PostgreSQL insert (#15647)
PostgreSQL rejects JSONB payloads containing \u0000 null bytes. Instead of catching the error after the fact, proactively strip them via a JdbcJsonbUtils utility used at all JSONB.valueOf() call sites.
1 parent 0465cfb commit 5ab3688

4 files changed

Lines changed: 71 additions & 2 deletions

File tree

jdbc-postgres/src/main/java/io/kestra/repository/postgres/PostgresRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.jooq.impl.DSL;
1717

1818
import io.kestra.core.repositories.ArrayListTotal;
19+
import io.kestra.jdbc.JdbcJsonbUtils;
1920
import io.kestra.jdbc.JdbcTableConfig;
2021
import io.kestra.jdbc.JooqDSLContextWrapper;
2122

@@ -57,7 +58,7 @@ public Condition fullTextCondition(List<String> fields, String query) {
5758
public Map<Field<Object>, Object> persistFields(T entity) {
5859
String json = MAPPER.writeValueAsString(entity);
5960
Map<Field<Object>, Object> fields = HashMap.newHashMap(1);
60-
fields.put(VALUE_FIELD, DSL.val(JSONB.valueOf(json)));
61+
fields.put(VALUE_FIELD, DSL.val(JdbcJsonbUtils.valueOf(json)));
6162
return fields;
6263
}
6364

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.kestra.jdbc;
2+
3+
import org.jooq.JSONB;
4+
5+
/**
6+
* Strips PostgreSQL-incompatible null bytes from JSONB payloads.
7+
*/
8+
public final class JdbcJsonbUtils {
9+
private JdbcJsonbUtils() {}
10+
11+
public static JSONB valueOf(String json) {
12+
return json == null ? null : JSONB.valueOf(json.replace("\u0000", ""));
13+
}
14+
}

jdbc/src/main/java/io/kestra/jdbc/runner/JdbcQueue.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.kestra.core.utils.ExecutorsUtils;
3333
import io.kestra.core.utils.IdUtils;
3434
import io.kestra.core.utils.ListUtils;
35+
import io.kestra.jdbc.JdbcJsonbUtils;
3536
import io.kestra.jdbc.JdbcMapper;
3637
import io.kestra.jdbc.JdbcTableConfigs;
3738
import io.kestra.jdbc.JooqDSLContextWrapper;
@@ -130,7 +131,7 @@ protected Map<Field<Object>, Object> produceFields(String consumerGroup, String
130131
Map<Field<Object>, Object> fields = HashMap.newHashMap(4);
131132
fields.put(TYPE_FIELD, queueType());
132133
fields.put(KEY_FIELD, key != null ? key : IdUtils.create());
133-
fields.put(VALUE_FIELD, JSONB.valueOf(new String(bytes)));
134+
fields.put(VALUE_FIELD, JdbcJsonbUtils.valueOf(new String(bytes)));
134135

135136
if (consumerGroup != null) {
136137
fields.put(CONSUMER_GROUP_FIELD, consumerGroup);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.kestra.jdbc;
2+
3+
import org.jooq.JSONB;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
class JdbcJsonbUtilsTest {
9+
@Test
10+
void shouldReturnNullForNullInput() {
11+
assertThat(JdbcJsonbUtils.valueOf(null)).isNull();
12+
}
13+
14+
@Test
15+
void shouldStripNullBytes() {
16+
// Given
17+
String jsonWithNullBytes = "{\"key\":\"value\u0000with\u0000nulls\"}";
18+
19+
// When
20+
JSONB result = JdbcJsonbUtils.valueOf(jsonWithNullBytes);
21+
22+
// Then
23+
assertThat(result).isNotNull();
24+
assertThat(result.data()).isEqualTo("{\"key\":\"valuewithnulls\"}");
25+
assertThat(result.data()).doesNotContain("\u0000");
26+
}
27+
28+
@Test
29+
void shouldLeaveCleanJsonUnchanged() {
30+
// Given
31+
String cleanJson = "{\"key\":\"value\",\"number\":42}";
32+
33+
// When
34+
JSONB result = JdbcJsonbUtils.valueOf(cleanJson);
35+
36+
// Then
37+
assertThat(result).isNotNull();
38+
assertThat(result.data()).isEqualTo(cleanJson);
39+
}
40+
41+
@Test
42+
void shouldHandleJsonWithOnlyNullByte() {
43+
// Given
44+
String onlyNullByte = "\u0000";
45+
46+
// When
47+
JSONB result = JdbcJsonbUtils.valueOf(onlyNullByte);
48+
49+
// Then
50+
assertThat(result).isNotNull();
51+
assertThat(result.data()).isEmpty();
52+
}
53+
}

0 commit comments

Comments
 (0)