Repository navigation
valkey/sql: skip non-callable onclose/onconnect instead of asserting #32249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { randomUUIDv7, RedisClient, spawn } from "bun"; | ||
| import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; | ||
| import { bunExe, bunRun } from "harness"; | ||
| import { bunEnv, bunExe, bunRun } from "harness"; | ||
| import { join } from "node:path"; | ||
| import { | ||
| ctx as _ctx, | ||
|
|
@@ -6901,3 +6901,33 @@ | |
| }); | ||
| }); | ||
| } | ||
|
|
||
| test("non-callable onclose/onconnect does not crash on connection failure", async () => { | ||
| const src = ` | ||
| const { RedisClient } = Bun; | ||
| const bad = [{}, null, 123, "str", true, []]; | ||
| const pending = []; | ||
| for (const v of bad) { | ||
| const client = new RedisClient("redis://127.0.0.1:1", { connectionTimeout: 50, maxRetries: 0 }); | ||
| client.onclose = v; | ||
| client.onconnect = v; | ||
|
Check warning on line 6913 in test/js/valkey/valkey.test.ts
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 This line doesn't actually exercise the new Extended reasoning...What the issue isThe new test sets Code path walkthroughConcretely, for each
Why this mattersCLAUDE.md says: "Confirm deleting each load-bearing clause of your fix breaks at least one test." Reverting the That said, the test name explicitly scopes itself to "on connection failure", which is accurate; the How to fixTwo options:
|
||
| pending.push(client.connect().then(() => 0, () => 1)); | ||
| } | ||
| Promise.all(pending).then(results => { | ||
| console.log("rejected " + results.reduce((a, b) => a + b, 0) + "/" + bad.length); | ||
| }); | ||
| `; | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "-e", src], | ||
| env: bunEnv, | ||
| stderr: "pipe", | ||
| stdout: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| expect({ stdout, stderr, signalCode: proc.signalCode, exitCode }).toEqual({ | ||
| stdout: "rejected 6/6\n", | ||
| stderr: "", | ||
| signalCode: null, | ||
| exitCode: 0, | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 nit: The
is_callable()filter is also applied toPostgresSQLConnectionandJSMySQLConnection, but onlyRedisClientis tested. This is largely fine — those native connection objects aren't directly user-constructible and the publicBun.sqllayer already rejects non-callableonclose/onconnectvia$isCallable(src/js/internal/sql/shared.ts:1940-1949) — so the PG/MySQL hunks are defense-in-depth rather than a user-reachable fix. Mentioning per CLAUDE.md's "every behavioral change ships an automated test" guidance in case you want to add coverage, but not blocking.Extended reasoning...
What this is
The PR applies the identical
.filter(|v| v.is_callable())guard to three classes —JSValkeyClient(RedisClient),PostgresSQLConnection, andJSMySQLConnection— but the regression test intest/js/valkey/valkey.test.tsonly exercisesRedisClient. CLAUDE.md:259 states "Every behavioral change ships an automated test in the same PR... even for obviously-correct one-liners", and CLAUDE.md:256 asks to "Cover the variant matrix... every sibling entry point receiving the same fix". On a literal reading, the Postgres/MySQL hunks are untested behavioral changes.Why this is only a nit (addressing the refutation)
One reviewer argued this isn't a real coverage gap because the Postgres/MySQL paths aren't user-reachable, and that argument is largely correct:
JSMySQLConnection::constructor(and the Postgres equivalent) throws "MySQLConnection cannot be constructed directly". UnlikeBun.RedisClient, users cannotnewthese and assign.onclose = {}.Bun.sqlentry point checks$isCallable(onclose)/$isCallable(onconnect)atsrc/js/internal/sql/shared.ts:1940-1949and throwsERR_INVALID_ARG_TYPEbefore any value reaches native code.onconnect/oncloseslots are written bycreateInstancefromarguments[9]/arguments[10], which come from Bun's internal pool wrapper (src/js/internal/sql/{mysql,postgres}.ts), not directly from user input.PooledConnectionwrapper; it is not surfaced through the publicsql()interface, so there's no documented way for user code to reach thecached_prop_hostfns!-generated.onclosesetter.Given (1)–(4), the Postgres/MySQL change is defense-in-depth guarding an internal invariant, not a fix for a user-reachable crash. CLAUDE.md's "sibling entry point" examples ("CLI flag AND JS API; direct AND proxy") enumerate user-facing variants; internal implementation classes that share a code pattern are a weaker fit for that rule.
Why it's still worth a mention
The same CLAUDE.md:259 paragraph also says "never add NEW production code solely to make a test writable", which cuts against fabricating internal access just to test this guard. However, the native classes do expose writable
.onclose/.onconnectsetters viacached_prop_hostfns!(JSMySQLConnection.rs:592-596, and the equivalent block in PostgresSQLConnection.rs / sql.classes.ts), so if any future code path hands a native connection reference to user code — or if the$isCallableguard inshared.tsis ever removed — the only thing preventing the original assertion crash on those adapters is this one-line filter, with no test pinning it. The Redis test does exercise the identical.filter(|v| v.is_callable())pattern, which gives reasonable confidence the mechanical guard works, but it wouldn't catch an independent revert of the PG/MySQL hunks.Step-by-step: why the PG/MySQL crash is not reachable today
Bun.sql({ adapter: 'postgres', onclose: {} }).src/js/internal/sql/shared.ts:1946-1949runsif (!$isCallable(onclose)) throw $ERR_INVALID_ARG_TYPE(...)→ throws before any native connection is created.onclose), the internal pool creates the native connection viacreateInstance(...)with a known-callable internal wrapper asarguments[10].PostgresSQLConnection/MySQLConnectionobject is stored on the internalPooledConnection; the user never receives a reference to it, so cannot call its native.onclosesetter with a non-callable.consume_on_close_callbackonly ever sees callable values from the public API, and the new.filter(|v| v.is_callable())is belt-and-suspenders.Contrast with Redis:
new Bun.RedisClient(url)is public, andclient.onclose = {}writes directly to the native cached slot with no JS-side validation — that path is user-reachable, which is why the fuzzer found it there and why the test correctly targets it.Suggested resolution
Non-blocking. If a Postgres/MySQL container fixture is already wired into the test suite (the repo has both under
test/js/sql/), a small test that obtains the internal connection handle (if an existingbun:internal-for-testingsurface exposes it) and sets.onclose = {}would close the gap. If no such surface exists, it's reasonable to leave this as-is — adding a test-only hook just for this would itself violate CLAUDE.md:259's "fault-injection flags and test-only hooks get the PR blocked" clause, and the Redis test already pins the shared filter pattern.