Skip to content

redis: propagate the real failure reason to the connect() promise - #35509

Open
robobun wants to merge 7 commits into
mainfrom
farm/f11edd7d/redis-connect-error-identity
Open

redis: propagate the real failure reason to the connect() promise#35509
robobun wants to merge 7 commits into
mainfrom
farm/f11edd7d/redis-connect-error-identity

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Problem

Four distinct connect-time failures all surfaced as the same ERR_REDIS_CONNECTION_CLOSED / "Connection closed" when awaited via client.connect():

wrong password (-WRONGPASS)                -> ERR_REDIS_CONNECTION_CLOSED | Connection closed
no auth to auth server (-NOAUTH)           -> ERR_REDIS_CONNECTION_CLOSED | Connection closed
connectionTimeout 400ms (silent server)    -> ERR_REDIS_CONNECTION_CLOSED | Connection closed
ECONNREFUSED (nothing listening)           -> ERR_REDIS_CONNECTION_CLOSED | Connection closed

A misconfigured password read exactly like a network blip, which leads to retry storms against a permanent auth failure and makes it impossible to tell "wrong password" from "server down" from "timeout".

Cause

handle_hello_response() and on_connection_timeout() already call fail() with the right message and RedisError (queued commands were rejected with the correct error). But fail() only rejects in-flight and queued commands; the connect() promise is held in connection_promise and is rejected later by on_valkey_close(), which always built a fresh generic "Connection closed" error regardless of what caused the close.

Fix

Store the first failure passed to fail() in a new close_reason field on ValkeyClient, and have on_valkey_close() use it (falling back to the generic error when none was recorded). on_connect_error() now records "Failed to connect" to distinguish a TCP-level connect failure. The field is reset alongside flags.failed in do_connect() and on_open().

After:

wrong password (-WRONGPASS)                -> ERR_REDIS_AUTHENTICATION_FAILED | WRONGPASS invalid username-password pair or user is disabled.
no auth to auth server (-NOAUTH)           -> ERR_REDIS_AUTHENTICATION_FAILED | NOAUTH HELLO must be called with the client already authenticated
connectionTimeout 400ms (silent server)    -> ERR_REDIS_CONNECTION_TIMEOUT | Connection timeout reached after 400ms
ECONNREFUSED (nothing listening)           -> ERR_REDIS_CONNECTION_CLOSED | Failed to connect

The onclose callback now also receives the specific error instead of the generic one.

Verification

New tests in test/js/valkey/reliability/connection-failures.test.ts use local net.createServer stubs for each case (no Docker needed). All four fail on the released binary and pass with this change.

Related: #23467 (this change will surface the real reason that connection fails instead of the generic ERR_REDIS_CONNECTION_CLOSED).


[review] gate passed · iteration 1 · 3 files touched

fails on main (without fix)
ASAN without fix: 4 failed, 13 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/reliability/connection-failures.test.ts
bun test v1.4.0 (fb01fcce5)

test/js/valkey/reliability/connection-failures.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle initial connection failure gracefully
(skip) Valkey: Connection Failures > Connection Failure Handling > should reject commands with appropriate errors when disconnected
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle connection timeout
(skip) Valkey: Connection Failures > Connection Failure Handling > should report correct connected status
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is enabled
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is dis
... (truncated)

release without fix: 4 failed, 13 skipped
bun test v1.4.0-canary.1 (643a531e0)

test/js/valkey/reliability/connection-failures.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle initial connection failure gracefully
(skip) Valkey: Connection Failures > Connection Failure Handling > should reject commands with appropriate errors when disconnected
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle connection timeout
(skip) Valkey: Connection Failures > Connection Failure Handling > should report correct connected status
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is enabled
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is disabled
(skip) Valkey: Connection Failures > Reconnection Behavior > should stop reconnection attempts after max retries
(skip) Valkey: Connection Failures > Connection Event Callba
... (truncated)
passes on PR (with fix)
ASAN with fix: 13 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/reliability/connection-failures.test.ts
bun test v1.4.0 (fb01fcce5)

test/js/valkey/reliability/connection-failures.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle initial connection failure gracefully
(skip) Valkey: Connection Failures > Connection Failure Handling > should reject commands with appropriate errors when disconnected
(skip) Valkey: Connection Failures > Connection Failure Handling > should handle connection timeout
(skip) Valkey: Connection Failures > Connection Failure Handling > should report correct connected status
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is enabled
(skip) Valkey: Connection Failures > Reconnection Behavior > should reject commands when offline queue is dis
... (truncated)

release with fix: 13 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     fb01fcce55
  features     baseline

22 deps, 108 codegen, 1171 objects in 903ms

ninja: Entering directory `/workspace/bun/build/release'
[1/214] gen ErrorCode+*.h
[2/214] fetch lsquic
[lsquic] up to date
[3/214] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[4/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_arr.c.o
[5/214] cc obj/vendor/lsquic/src/liblsquic/ls-sfparser.c.o
[6/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_di_error.c.o
[7/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_crand.c.o
[8/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_cfcw.c.o
[9/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_enc_sess_common.c.o
[10/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_alarmset.c.o
[11/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_attq.c.o
[12/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_adaptive_cc.c.o
[13/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_eng_hist.c.o
[14/214] cc obj/vendor/lsquic/src/liblsquic/lsquic_frab_list.c.o
[15/214] cc obj/
... (truncated)
diff hotspot
src/runtime/valkey_jsc/js_valkey.rs                | 27 ++++--
 src/runtime/valkey_jsc/valkey.rs                   | 10 +++
 .../valkey/reliability/connection-failures.test.ts | 95 ++++++++++++++++++++++
 3 files changed, 126 insertions(+), 6 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                                    reads  edits  tests
src/runtime/valkey_jsc/js_valkey.rs                         7      6      0
src/runtime/valkey_jsc/valkey.rs                            5      3      0
test/js/valkey/reliability/connection-failures.test.ts      1      7      0

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Valkey connection error identity

Layer / File(s) Summary
Failure state capture and reset
src/runtime/valkey_jsc/valkey.rs
ValkeyClient records the first failure reason and resets it when a socket opens.
JavaScript lifecycle propagation
src/runtime/valkey_jsc/js_valkey.rs
Connection setup clears stale reasons, and close handling converts recorded reasons into JavaScript errors with a generic fallback.
Error identity coverage
test/js/valkey/reliability/connection-failures.test.ts
Tests cover HELLO authentication failures, connection timeouts, and TCP connection failures with their expected codes and messages.

Possibly related PRs

  • oven-sh/bun#34714: Changes the same socket lifecycle handlers with ref-scope and refcount lifetime fixes.
  • oven-sh/bun#34829: Updates Valkey connection lifecycle error handling in the same runtime area.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly states the main change: propagating the real failure reason to connect().
Description check ✅ Passed The PR description covers the problem, cause, fix, and verification, so it satisfies the template's intent.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:11 PM PT - Jul 24th, 2026

@robobun, your commit fb01fcc has 1 failures in Build #80065 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35509

That installs a local version of the PR into your bun-35509 executable, so you can run:

bun-35509 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Connection to redis fails on prod 7.4 redis redislabs server (local redis works fine) #23467 - User gets generic ERR_REDIS_CONNECTION_CLOSED when connecting to Redis Labs production server; this PR replaces that catch-all with the actual failure reason, which would reveal the real problem

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #23467

🤖 Generated with Claude Code

Comment thread src/runtime/valkey_jsc/js_valkey.rs
Comment thread src/runtime/valkey_jsc/js_valkey.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/valkey/reliability/connection-failures.test.ts`:
- Line 341: Remove the implementation-history comment near the reliability
regression test; retain the test name and existing behavior unchanged, since
only the issue URL comment should remain for this regression test.
- Around line 408-409: Update the timeout error assertion in the
connection-failure test to require the exact configured message “Connection
timeout reached after 200ms” instead of using a substring assertion. Keep the
existing error-code assertion unchanged.
- Around line 344-354: The stubServer helper currently treats arbitrary TCP
chunks as complete requests, suppresses socket errors, and exposes non-awaitable
cleanup. Update stubServer to frame requests or respond only after the initial
request is complete, propagate server and socket lifecycle failures through the
relevant promises, and return an async close() that awaits server shutdown.
Update every finally block using stubServer to await close() so all fixture
resources are released.
- Around line 415-428: Add a two-attempt reliability test alongside the existing
connectError test that enables autoReconnect, causes the first connection
attempt to fail with an auth or timeout reason, then causes the reconnect
attempt to end with a different terminal failure. Assert the final connectError
result uses the second attempt’s error code and message, covering stale-reason
reset behavior while preserving the existing single-failure coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb87f14b-694d-4f7e-ab2c-c994b3f2828d

📥 Commits

Reviewing files that changed from the base of the PR and between ae4b17d and 643a531.

📒 Files selected for processing (3)
  • src/runtime/valkey_jsc/js_valkey.rs
  • src/runtime/valkey_jsc/valkey.rs
  • test/js/valkey/reliability/connection-failures.test.ts

Comment thread test/js/valkey/reliability/connection-failures.test.ts
Comment thread test/js/valkey/reliability/connection-failures.test.ts Outdated
Comment thread test/js/valkey/reliability/connection-failures.test.ts Outdated
Comment thread test/js/valkey/reliability/connection-failures.test.ts
robobun and others added 7 commits July 25, 2026 02:54
A -WRONGPASS or -NOAUTH reply to HELLO, a connectionTimeout expiry, and
a TCP connect error all surfaced as the same
ERR_REDIS_CONNECTION_CLOSED / "Connection closed" when awaited via
client.connect(). The server's actual error text was discarded.

The failure was already passed to fail() which rejected queued commands
with the right message, but on_valkey_close() then built a fresh generic
error for the connection_promise and the onclose callback.

Store the first failure in close_reason and use it in on_valkey_close()
so connect() rejects with ERR_REDIS_AUTHENTICATION_FAILED + the server's
WRONGPASS/NOAUTH text, ERR_REDIS_CONNECTION_TIMEOUT for a
connectionTimeout, and "Failed to connect" for a TCP connect error.

Fixes #3064
on_connect_error under autoReconnect left the stale "Failed to connect"
close_reason alive into the next attempt; if that attempt then timed out
before on_open(), the connect() promise reported the stale reason instead
of ERR_REDIS_CONNECTION_TIMEOUT.
The stub only answers the initial HELLO so it now buffers until the full
CRLF-terminated command is received and replies exactly once, avoiding
any reliance on a single-write TCP read boundary.
@robobun
robobun force-pushed the farm/f11edd7d/redis-connect-error-identity branch from a904270 to fb01fcc Compare July 25, 2026 03:00
Comment thread src/runtime/valkey_jsc/js_valkey.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the stale-close_reason reset I flagged earlier is now handled in connect(), and the two remaining items (TLS handshake sibling, errno in "Failed to connect") are reasonable follow-ups that match the Postgres/MySQL sibling convention.

What was reviewed:

  • close_reason set/clear points cover every attempt entry (do_connect, connect, on_open) and every terminal exit (fail first-writer-wins → on_valkey_close().take()); traced the auto-reconnect path to confirm no stale-reason carryover.
  • Box<[u8]> in close_reason is freed by .take() or field Drop — no leak.
  • New tests are hermetic (local net.createServer, not gated on Docker); the stub now buffers until HELLO+CRLF before replying, and the timeout test asserts the exact message.
Extended reasoning...

Overview

Adds a close_reason: Option<(Box<[u8]>, RedisError)> field to ValkeyClient so the connect() promise (and onclose callback) reject with the actual failure cause instead of a generic "Connection closed". fail() records the first failure; on_valkey_close() consumes it via .take(); on_connect_error() records "Failed to connect" for the TCP-level case. The field is reset alongside other per-attempt state in do_connect(), the private connect(), and on_open(). Four new hermetic tests exercise WRONGPASS, NOAUTH, connectionTimeout, and ECONNREFUSED using local net.createServer stubs.

Security risks

None. This is error-message plumbing on failure paths; no new parsing of untrusted input, no auth/crypto/permission changes. The recorded message bytes originate from the server's HELLO error line (already displayed to users via queued-command rejections before this PR) or from static literals.

Level of scrutiny

Moderate. The valkey client has intrusive refcounting and re-entrant close/fail paths, so any new mutable per-connection state needs its reset points enumerated. I traced them across two prior review passes: my earlier 🔴 (stale reason surviving into an auto-reconnect attempt because reconnect()connect() didn't clear it) was fixed in 8610e6e / 643a531. The remaining lifecycle is: fail() guards on is_none() (first-writer wins), fail_with_js_value() sets is_manually_closed and closes the socket, on_close() re-enters fail() which now early-returns on flags.failed, and on_valkey_close() .take()s the reason exactly once. The Box<[u8]> drops naturally with the struct if never taken.

Other factors

  • All prior review threads (mine and CodeRabbit's) are resolved. The two acknowledged follow-ups — TLS handshake failure still surfaces as generic (needs close_reason to hold a JSValue, not just bytes) and on_connect_error discarding the errno — are unchanged from pre-PR behavior and match the Postgres/MySQL siblings exactly.
  • The new describe block is not gated on isEnabled, so the tests run on every CI lane without Docker; the gate evidence confirms 4 fail on the released binary and pass with the fix.
  • The stub-server framing concern was addressed (buffers until HELLO + trailing CRLF, replies once); the timeout assertion is now exact.
  • No changes since my last inline comment other than the CI-retrigger commit.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI build 80065 failed on agent availability, not on this diff:

  • All 8 build-bun failures are "Sibling step build-cpp errored, nothing to link" after the build-cpp jobs expired waiting for an agent (17 lanes had their initial job expire).
  • 3 build-bun jobs timed out (x64-musl, windows-x64, windows-aarch64).
  • The Rust compile (cargo build -p bun_bin) succeeded on every lane that reached it.
  • linux-aarch64-musl and freebsd-aarch64 built and linked successfully; all alpine aarch64 test-bun shards passed, including test/js/valkey/reliability/connection-failures.test.ts.

This diff touches src/runtime/valkey_jsc/*.rs and one test file only; no C++. Verified locally: the four new tests fail on the released binary and pass on bun bd test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants