fix: ClassCastException when follower forwards positional-param command to leader#3961
fix: ClassCastException when follower forwards positional-param command to leader#3961
Conversation
…nd to leader
When a Raft follower forwarded a write command with positional parameters
(e.g. INSERT with ?-placeholders), the params were serialized as a JSON array
[110]. The leader's PostCommandHandler called json.toMap(true), which converted
the numeric array to float[]{110.0} via the vector-embedding optimization
introduced in #3864. The subsequent cast (Map<String,Object>) float[] then threw
ClassCastException: class [F cannot be cast to class java.util.Map.
Fix 1 (RaftReplicatedDatabase): serialize positional args as an ordinal map
{"0": v0, "1": v1, ...} instead of a JSON array, matching the canonical format
produced by RemoteDatabase.mapArgs() and expected by mapParams() on the leader.
Fix 2 (PostCommandHandler): defensive handling for the case where params is a
List or primitive array, converting it to an ordinal map so the handler is
robust regardless of how params arrive.
Regression test added to RaftLeaderProxyIT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 25 |
🟢 Coverage 36.84% diff coverage · -7.25% coverage variation
Metric Results Coverage variation ✅ -7.25% coverage variation Diff coverage ✅ 36.84% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (bd33f49) 118934 86428 72.67% Head commit (df6b830) 150526 (+31592) 98474 (+12046) 65.42% (-7.25%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#3961) 19 7 36.84% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes. Give us feedback
There was a problem hiding this comment.
Code Review
This pull request addresses a ClassCastException that occurs when positional parameters are proxied to the Raft leader. The fix involves serializing positional arguments as an ordinal map in RaftReplicatedDatabase and updating PostCommandHandler to robustly handle parameters provided as Maps, Lists, or primitive arrays. A regression test has been added to verify the fix. Feedback suggests enhancing the regression test to specifically include an all-numeric array to trigger the float[] optimization path and optimizing HashMap initializations in PostCommandHandler by accounting for the default load factor.
| final JSONObject body = new JSONObject() | ||
| .put("language", "sql") | ||
| .put("command", "INSERT INTO V1 SET id = ?, name = ?") | ||
| .put("params", new com.arcadedb.serializer.json.JSONArray().put(42).put("positional-test")); |
There was a problem hiding this comment.
The current regression test uses a mixed-type array [42, "positional-test"]. While this correctly tests the List handling in PostCommandHandler, it does not trigger the float[] optimization in json.toMap(true) which was the specific cause of the ClassCastException mentioned in the PR description (where an all-numeric array like [110] is converted to a primitive array).
Consider adding a second test case or updating this one to use an all-numeric array to ensure the float[] code path in PostCommandHandler is also covered by the regression suite.
There was a problem hiding this comment.
Added in df6b830. The test now sends a second request with params: [42] (all-numeric) via the follower, which exercises the float[] branch in PostCommandHandler. The assertion verifies the response is 200 rather than a 500 ClassCastException.
| paramMap = new HashMap<>(list.size()); | ||
| for (int i = 0; i < list.size(); i++) | ||
| paramMap.put("" + i, list.get(i)); |
There was a problem hiding this comment.
When initializing a HashMap with a known size, it is recommended to account for the default load factor (0.75) to avoid an immediate resize when the map is filled. For a list of size n, the initial capacity should be (int) (n / 0.75f) + 1.
| paramMap = new HashMap<>(list.size()); | |
| for (int i = 0; i < list.size(); i++) | |
| paramMap.put("" + i, list.get(i)); | |
| paramMap = new HashMap<>((int) (list.size() / 0.75f) + 1); | |
| for (int i = 0; i < list.size(); i++) | |
| paramMap.put("" + i, list.get(i)); |
| paramMap = new HashMap<>(len); | ||
| for (int i = 0; i < len; i++) | ||
| paramMap.put("" + i, java.lang.reflect.Array.get(rawParams, i)); |
There was a problem hiding this comment.
Similar to the List case, the HashMap for primitive arrays should be initialized with a capacity that accounts for the load factor to prevent unnecessary resizing.
| paramMap = new HashMap<>(len); | |
| for (int i = 0; i < len; i++) | |
| paramMap.put("" + i, java.lang.reflect.Array.get(rawParams, i)); | |
| paramMap = new HashMap<>((int) (len / 0.75f) + 1); | |
| for (int i = 0; i < len; i++) | |
| paramMap.put("" + i, java.lang.reflect.Array.get(rawParams, i)); |
…aces Three related issues fixed in Docker-based Raft HA tests: - DatabaseWrapper.createDatabase(): restore FIXED strategy to prevent the client from following cluster-reported internal Docker addresses (arcadedb-N:2480) after failures. Broaden the retry loop to cover both ServerIsNotTheLeaderException with null leader (post-election window) and general connection failures (server temporarily down during rolling restart). - ContainersTestTemplate: add waitForAllNodesKnowLeader() helper that polls /api/v1/cluster on all nodes until every one reports a non-null leaderHttpAddress. Bridges the window where one node is already leader but followers haven't yet propagated the leader address. - SplitBrainIT.splitBrainPrevention(): call waitForAllNodesKnowLeader() after waitForRaftLeader() before issuing writes, and increase the replication check timeout from 30s to 2 minutes. Without the leader-address wait, addUserAndPhotos() silently dropped writes because the follower's forwardCommandToLeaderViaRaft() failed with a null leader address. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s from prior test run GroupManagementIT creates testuser without cleanup; FileUtils.deleteRecursively silently fails to delete ./target/config/, leaving stale server-users.jsonl that causes CREATE USER to return 403 on the next run. Dropping the users at test start makes the test idempotent against leftover state. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The bd33f49 commit added FileUtils.deleteRecursively("./target/config/") to TestServerHelper.deleteDatabaseFolders(), which runs between setTestConfiguration() and startServers() in @beforeeach. This wiped the gremlin-server.yaml, gremlin-server.properties, and gremlin-server.groovy files before the server could read them, so the Gremlin plugin fell back to default settings with no graphs and no traversal sources, causing "traversal source [graph] not configured" failures. Fix: move config file writing to onBeforeStarting(), which is called inside startServers() after folder cleanup but before server.start(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…verage - Use (int)(n/0.75f)+1 initial capacity for the List<> and primitive-array positional-param HashMaps to avoid rehashing when the map is filled - Extend positionalParamsViaFollowerIsProxiedToLeader to also send an all-numeric array [42] via a follower, exercising the float[] code path that was the original ClassCastException trigger Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3961 +/- ##
==========================================
+ Coverage 63.84% 64.44% +0.60%
==========================================
Files 1591 1593 +2
Lines 118934 119287 +353
Branches 25275 25384 +109
==========================================
+ Hits 75929 76872 +943
+ Misses 32536 31755 -781
- Partials 10469 10660 +191 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…skip ci] Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.10 to 42.7.11. Release notes *Sourced from [org.postgresql:postgresql's releases](https://github.com/pgjdbc/pgjdbc/releases).* > v42.7.11 > -------- > > Security > -------- > > * fix: Limit SCRAM PBKDF2 iterations accepted from the server. > pgjdbc was vulnerable to a client-side denial of service in SCRAM-SHA-256 authentication, where a malicious or compromised PostgreSQL server could specify an extremely large PBKDF2 iteration count, causing the client to consume unbounded CPU and potentially exhaust connection pools. The fix introduces a new scramMaxIterations connection property (defaulting to 100,000) to cap iteration counts before computation begins. > See the [Security Advisory](GHSA-98qh-xjc8-98pq) for more detail. > The following [CVE-2026-42198](https://nvd.nist.gov/vuln/detail/CVE-2026-42198) has been issued. > > Changes > ------- > > * fix: Add sources and javadocs to shaded published lib generation [`@sehrope`](https://github.com/sehrope) ([#4043](https://redirect.github.com/pgjdbc/pgjdbc/issues/4043)) > * update Changelog and website for release of 42.7.11 [`@davecramer`](https://github.com/davecramer) ([#4042](https://redirect.github.com/pgjdbc/pgjdbc/issues/4042)) > * Fix scram fix location in changelog and update published artifact developer list [`@sehrope`](https://github.com/sehrope) ([#4041](https://redirect.github.com/pgjdbc/pgjdbc/issues/4041)) > * Restrict test with scram\_iterations to v16+ and release notes [`@sehrope`](https://github.com/sehrope) ([#4040](https://redirect.github.com/pgjdbc/pgjdbc/issues/4040)) > * chore(deps): update ubuntu:24.04 docker digest to 84e77de [`@renovate-bot`](https://github.com/renovate-bot) ([#4017](https://redirect.github.com/pgjdbc/pgjdbc/issues/4017)) > * test: add tests for QueryExecutor#getTransactionState [`@vlsi`](https://github.com/vlsi) ([#4006](https://redirect.github.com/pgjdbc/pgjdbc/issues/4006)) > * chore(deps): update actions/create-github-app-token action to v2.2.2 [`@renovate-bot`](https://github.com/renovate-bot) ([#3983](https://redirect.github.com/pgjdbc/pgjdbc/issues/3983)) > * fix: fix flaky CopyBothResponseTest by using WAL flush LSN [`@vlsi`](https://github.com/vlsi) ([#3979](https://redirect.github.com/pgjdbc/pgjdbc/issues/3979)) > * fix: fix flaky replication restart tests by waiting for confirmed\_flush\_lsn [`@vlsi`](https://github.com/vlsi) ([#3975](https://redirect.github.com/pgjdbc/pgjdbc/issues/3975)) > * test: fix flaky LogicalReplicationStatusTest by polling pg\_stat\_replication [`@vlsi`](https://github.com/vlsi) ([#3974](https://redirect.github.com/pgjdbc/pgjdbc/issues/3974)) > * chore: replace Appveyor with ikalnytskyi/action-setup-postgres [`@vlsi`](https://github.com/vlsi) ([#3966](https://redirect.github.com/pgjdbc/pgjdbc/issues/3966)) > * test: move test table creation from [`@BeforeEach`](https://github.com/BeforeEach) to [`@BeforeAll`](https://github.com/BeforeAll) [`@vlsi`](https://github.com/vlsi) ([#3967](https://redirect.github.com/pgjdbc/pgjdbc/issues/3967)) > * Return jsonb as PGObject fixes Issue [#3926](https://redirect.github.com/pgjdbc/pgjdbc/issues/3926) [`@davecramer`](https://github.com/davecramer) ([#3956](https://redirect.github.com/pgjdbc/pgjdbc/issues/3956)) > * Update docker scripts [`@davecramer`](https://github.com/davecramer) ([#3958](https://redirect.github.com/pgjdbc/pgjdbc/issues/3958)) > * implement require\_auth, this is pretty much how libpq does this. [`@davecramer`](https://github.com/davecramer) ([#3895](https://redirect.github.com/pgjdbc/pgjdbc/issues/3895)) > * docs: add SCRAM authentication test setup section to TESTING.md [`@emmaeng700`](https://github.com/emmaeng700) ([#3945](https://redirect.github.com/pgjdbc/pgjdbc/issues/3945)) > * Add RequireServerVersion annotation for tests [`@sehrope`](https://github.com/sehrope) ([#3939](https://redirect.github.com/pgjdbc/pgjdbc/issues/3939)) > > 🐛 Bug Fixes > ----------- > > * fix: ensure extended protocol messages end with Sync message [`@vlsi`](https://github.com/vlsi) ([#3728](https://redirect.github.com/pgjdbc/pgjdbc/issues/3728)) > * fix: enable cursor-based fetching in extended protocol when transaction started via SQL command [`@vlsi`](https://github.com/vlsi) ([#3996](https://redirect.github.com/pgjdbc/pgjdbc/issues/3996)) > * fix: retry with SSL on IOException when sslMode=ALLOW [`@vlsi`](https://github.com/vlsi) ([#3973](https://redirect.github.com/pgjdbc/pgjdbc/issues/3973)) > * fix: allow fallback to non-SSL connection when sslMode=prefer and sslResponseTimeout kicks in [`@vlsi`](https://github.com/vlsi) ([#3968](https://redirect.github.com/pgjdbc/pgjdbc/issues/3968)) > * fix: catch SecurityException from setContextClassLoader on ForkJoinPool workers [`@vlsi`](https://github.com/vlsi) ([#3962](https://redirect.github.com/pgjdbc/pgjdbc/issues/3962)) > * fix: use compareTo for LogSequenceNumber comparison [`@vlsi`](https://github.com/vlsi) ([#3961](https://redirect.github.com/pgjdbc/pgjdbc/issues/3961)) > * fix: release COPY lock on IOException to prevent connection hang ([#3957](https://redirect.github.com/pgjdbc/pgjdbc/issues/3957)) [`@vlsi`](https://github.com/vlsi) ([#3960](https://redirect.github.com/pgjdbc/pgjdbc/issues/3960)) > > 🧰 Maintenance > ------------- > > * style: replace [`@exception`](https://github.com/exception) with [`@throws`](https://github.com/throws) in getBoolean javadoc [`@vlsi`](https://github.com/vlsi) ([#4035](https://redirect.github.com/pgjdbc/pgjdbc/issues/4035)) > * chore: use `@vlsi/github-actions-random-matrix` npm package [`@vlsi`](https://github.com/vlsi) ([#4008](https://redirect.github.com/pgjdbc/pgjdbc/issues/4008)) > * chore: use tag names for pinning github actions, pin ikalnytskyi/action-setup-postgres [`@vlsi`](https://github.com/vlsi) ([#4007](https://redirect.github.com/pgjdbc/pgjdbc/issues/4007)) > * chore: bump errorprone to 2.48.0 [`@vlsi`](https://github.com/vlsi) ([#4005](https://redirect.github.com/pgjdbc/pgjdbc/issues/4005)) > * test: add [`@DisableLogger`](https://github.com/DisableLogger) annotation to suppress expected log warnings in tests [`@vlsi`](https://github.com/vlsi) ([#3971](https://redirect.github.com/pgjdbc/pgjdbc/issues/3971)) > * chore: suppress deprecations in test code to reduce build verbosity [`@vlsi`](https://github.com/vlsi) ([#3972](https://redirect.github.com/pgjdbc/pgjdbc/issues/3972)) > * chore: replace log warning in ConnectionFactory.closeStream with Throwable.addSuppressed [`@vlsi`](https://github.com/vlsi) ([#3970](https://redirect.github.com/pgjdbc/pgjdbc/issues/3970)) > * chore: use greedy pairwise coverage for CI matrix generation [`@vlsi`](https://github.com/vlsi) ([#3965](https://redirect.github.com/pgjdbc/pgjdbc/issues/3965)) > * chore: use full version tags in GitHub Actions comments [`@vlsi`](https://github.com/vlsi) ([#3963](https://redirect.github.com/pgjdbc/pgjdbc/issues/3963)) > > ⬆️ Dependencies > --------------- ... (truncated) Changelog *Sourced from [org.postgresql:postgresql's changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md).* > [42.7.11] (2026-04-28) > ---------------------- > > ### Security > > * fix: Limit SCRAM PBKDF2 iterations accepted from the server. > pgjdbc was vulnerable to a client-side denial of service in SCRAM-SHA-256 authentication, where a malicious or compromised PostgreSQL server could specify an extremely large PBKDF2 iteration count, causing the client to consume unbounded CPU and potentially exhaust connection pools. The fix introduces a new scramMaxIterations connection property (defaulting to 100,000) to cap iteration counts before computation begins. > See the [Security Advisory](GHSA-98qh-xjc8-98pq) for more detail. > The following [CVE-2026-42198](https://nvd.nist.gov/vuln/detail/CVE-2026-42198) has been issued. > > ### Added > > * feat: implement require\_auth connection property, aligning with libpq behavior [PR [#3895](https://redirect.github.com/pgjdbc/pgjdbc/issues/3895)]([pgjdbc/pgjdbc#3895](https://redirect.github.com/pgjdbc/pgjdbc/pull/3895)) > > ### Changed > > * chore: replace Appveyor CI with ikalnytskyi/action-setup-postgres [PR [#3966](https://redirect.github.com/pgjdbc/pgjdbc/issues/3966)]([pgjdbc/pgjdbc#3966](https://redirect.github.com/pgjdbc/pgjdbc/pull/3966)) > * chore: upgrade Gradle to v9 [PR [#3978](https://redirect.github.com/pgjdbc/pgjdbc/issues/3978)]([pgjdbc/pgjdbc#3978](https://redirect.github.com/pgjdbc/pgjdbc/pull/3978)) > > ### Fixed > > * fix: ensure extended protocol messages end with Sync message [PR [#3728](https://redirect.github.com/pgjdbc/pgjdbc/issues/3728)]([pgjdbc/pgjdbc#3728](https://redirect.github.com/pgjdbc/pgjdbc/pull/3728)) > * fix: enable cursor-based fetching in extended protocol when transaction started via SQL command [PR [#3996](https://redirect.github.com/pgjdbc/pgjdbc/issues/3996)]([pgjdbc/pgjdbc#3996](https://redirect.github.com/pgjdbc/pgjdbc/pull/3996)) > * fix: retry with SSL on IOException when sslMode=ALLOW [PR [#3973](https://redirect.github.com/pgjdbc/pgjdbc/issues/3973)]([pgjdbc/pgjdbc#3973](https://redirect.github.com/pgjdbc/pgjdbc/pull/3973)) > * fix: make sure the driver honours connectTimeout when retrying the connection [PR [#3968](https://redirect.github.com/pgjdbc/pgjdbc/issues/3968)]([pgjdbc/pgjdbc#3968](https://redirect.github.com/pgjdbc/pgjdbc/pull/3968)) > * fix: allow fallback to non-SSL connection when sslMode=prefer and sslResponseTimeout kicks in [PR [#3968](https://redirect.github.com/pgjdbc/pgjdbc/issues/3968)]([pgjdbc/pgjdbc#3968](https://redirect.github.com/pgjdbc/pgjdbc/pull/3968)) > * fix: catch SecurityException from setContextClassLoader on ForkJoinPool workers [PR [#3962](https://redirect.github.com/pgjdbc/pgjdbc/issues/3962)]([pgjdbc/pgjdbc#3962](https://redirect.github.com/pgjdbc/pgjdbc/pull/3962)) > * fix: use compareTo for LogSequenceNumber comparison to handle unsigned values correctly [PR [#3961](https://redirect.github.com/pgjdbc/pgjdbc/issues/3961)]([pgjdbc/pgjdbc#3961](https://redirect.github.com/pgjdbc/pgjdbc/pull/3961)) > * fix: release COPY lock on IOException to prevent connection hang [PR [#3957](https://redirect.github.com/pgjdbc/pgjdbc/issues/3957)]([pgjdbc/pgjdbc#3957](https://redirect.github.com/pgjdbc/pgjdbc/pull/3957)) > * fix: return jsonb as PGObject instead of String [PR [#3956](https://redirect.github.com/pgjdbc/pgjdbc/issues/3956)]([pgjdbc/pgjdbc#3956](https://redirect.github.com/pgjdbc/pgjdbc/pull/3956)) > * fix: align SSL key file permission check with libpq [PR [#3952](https://redirect.github.com/pgjdbc/pgjdbc/issues/3952)]([pgjdbc/pgjdbc#3952](https://redirect.github.com/pgjdbc/pgjdbc/pull/3952)) > * fix: guard connection closed flag with a reentrant lock to protect against concurrent close [PR [#3905](https://redirect.github.com/pgjdbc/pgjdbc/issues/3905)]([pgjdbc/pgjdbc#3905](https://redirect.github.com/pgjdbc/pgjdbc/pull/3905)) Commits * [`78e261f`](pgjdbc/pgjdbc@78e261f) fix: Add sources and javadocs to shaded published lib generation * [`1e09fa0`](pgjdbc/pgjdbc@1e09fa0) update Changelog and website for release of 42.7.11 ([#4042](https://redirect.github.com/pgjdbc/pgjdbc/issues/4042)) * [`d479fa5`](pgjdbc/pgjdbc@d479fa5) Fix scram fix location in changelog and update published artifact developer l... * [`b04fc46`](pgjdbc/pgjdbc@b04fc46) docs: Add scram max iters fix to changelog * [`cf54822`](pgjdbc/pgjdbc@cf54822) test: Disable scram test on older version without scram\_iterations GUC * [`7dbcc79`](pgjdbc/pgjdbc@7dbcc79) test: Add SCRAM max iteration tests * [`c9d41d1`](pgjdbc/pgjdbc@c9d41d1) fix: Limit SCRAM PBKDF2 iterations accepted from the server * [`a340cb2`](pgjdbc/pgjdbc@a340cb2) style: replace [`@exception`](https://github.com/exception) with [`@throws`](https://github.com/throws) in getBoolean javadoc * [`77837f8`](pgjdbc/pgjdbc@77837f8) fix(deps): update dependency org.openrewrite.rewrite:org.openrewrite.rewrite.... * [`23af03b`](pgjdbc/pgjdbc@23af03b) chore(deps): update actions/checkout action to v6 * Additional commits viewable in [compare view](pgjdbc/pgjdbc@REL42.7.10...REL42.7.11) [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- Dependabot commands and options You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Summary
INSERT INTO User SET id = ?), the params were serialized as a JSON array[110]. The leader'sPostCommandHandlercalledjson.toMap(true), which converted the all-numeric array tofloat[]{110.0}via the vector-embedding optimization from Cypher Batch creation is slow with vector indexes #3864. The subsequent cast(Map<String,Object>) float[]threwClassCastException: class [F cannot be cast to class java.util.Map.RaftReplicatedDatabase): serialize positional args as an ordinal map{"0": v0, "1": v1, ...}instead of a plain JSON array, matching the canonical format produced byRemoteDatabase.mapArgs()and expected bymapParams()on the leader.PostCommandHandler): defensive handling for whenparamsis aListor primitive array - converts to ordinal map, making the handler robust regardless of how params arrive.Test Plan
positionalParamsViaFollowerIsProxiedToLeaderinRaftLeaderProxyIT- sends a parameterized INSERT via a follower's HTTP endpoint, verifies the record is committed and replicated to all nodesRaftLeaderProxyITpass (ddlViaFollowerIsProxiedToLeader,dmlViaFollowerIsProxiedToLeader,positionalParamsViaFollowerIsProxiedToLeader)🤖 Generated with Claude Code