redis: reject file-backed Blob arguments instead of sending the empty string

RO
17d ago
+140-1

What does this PR do?

RedisClient accepts Blob for any KeyLike parameter (key or value). The argument converter read the Blob's bytes via shared_view(), which is empty for a file- or S3-backed store. So:

await redis.set("tpl", Bun.file("page.html"));

sent SET "tpl" "" on the wire, resolved "OK", and silently dropped the file contents. The same applied to:

  • a Bun.file() as a key (became the empty key "")
  • a single-part new Blob([Bun.file(p)]) (shares the file store)
  • a nonexistent path (no ENOENT, just "")

Wire-verified on stock 1.4.0-canary and release-asan main; deterministic.

Cause

js_valkey_functions.rs::from_js calls BlobOrStringOrBuffer::from_js_maybe_file(global, value, false). The false skips the file-blob guard in from_js_maybe_file_maybe_async, so a file-backed Blob falls through to Self::Blob(blob.dupe()) and slice() returns its empty shared_view(). (The allow_file parameter there is named the opposite of its effect; left alone to keep the diff small.)

Fix

After conversion, check needs_to_read_file() || is_s3() on the Blob arm and throw ERR_INVALID_ARG_TYPE with "pass await blob.bytes() instead", matching ServerWebSocket.send and the MySQL parameter binder. All ~50 prototype methods and .send() route through this one helper, so the check covers every argument position.

Not changed

A Uint8Array over a detached ArrayBuffer still serializes as "". Per spec a detached view has byteLength === 0, and Node treats it as empty in comparable APIs, so sending zero bytes is arguably correct; happy to reject it too if preferred.

How did you verify your code works?

Added two dockerless tests to test/js/valkey/unit/buffer-operations.test.ts that stand up a minimal RESP3 recorder:

  • in-memory / sliced Blobs reach the wire with their bytes intact (regression guard)
  • Bun.file() as value, as key, wrapped, after .text(), nonexistent path, and via getBuffer/append all throw and produce no wire frame
USE_SYSTEM_BUN=1 bun test buffer-operations.test.ts -t "Blob argument"   # 1 pass, 1 fail
bun bd test buffer-operations.test.ts -t "Blob argument"                 # 2 pass

[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 failed, 9 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/unit/buffer-operations.test.ts
bun test v1.4.0 (23619b79a)

test/js/valkey/unit/buffer-operations.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: Buffer Operations > getBuffer returns binary data as Uint8Array
(skip) Valkey: Buffer Operations > getBuffer for non-existent key returns null
(skip) Valkey: Buffer Operations > Really long buffer
(skip) Valkey: Buffer Operations > Buffer with no bytes
(skip) Valkey: Buffer Operations > Buffer with null bytes
(skip) Valkey: Buffer Operations > concurrent getBuffer against large blob
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBufferView key
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBuffer key
(skip) Valkey: Buffer Operations > set and getBuffer with Blob key
(pass) Valkey: Blob argument serialization > in-memory Blob arguments reach th
... (truncated)

release without fix: 1 failed, 9 skipped
bun test v1.4.0-canary.1 (1498d7b77)

test/js/valkey/unit/buffer-operations.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: Buffer Operations > getBuffer returns binary data as Uint8Array
(skip) Valkey: Buffer Operations > getBuffer for non-existent key returns null
(skip) Valkey: Buffer Operations > Really long buffer
(skip) Valkey: Buffer Operations > Buffer with no bytes
(skip) Valkey: Buffer Operations > Buffer with null bytes
(skip) Valkey: Buffer Operations > concurrent getBuffer against large blob
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBufferView key
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBuffer key
(skip) Valkey: Buffer Operations > set and getBuffer with Blob key
(pass) Valkey: Blob argument serialization > in-memory Blob arguments reach the wire with their bytes intact [1.76ms]
209 |       const expected = expect.objectContaining({
210 |         code: "ERR_INVALID_ARG_TYPE",
211 |         message: expect.s
... (truncated)
passes on PR (with fix)
ASAN with fix: 9 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/unit/buffer-operations.test.ts
bun test v1.4.0 (23619b79a)

test/js/valkey/unit/buffer-operations.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: Buffer Operations > getBuffer returns binary data as Uint8Array
(skip) Valkey: Buffer Operations > getBuffer for non-existent key returns null
(skip) Valkey: Buffer Operations > Really long buffer
(skip) Valkey: Buffer Operations > Buffer with no bytes
(skip) Valkey: Buffer Operations > Buffer with null bytes
(skip) Valkey: Buffer Operations > concurrent getBuffer against large blob
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBufferView key
(skip) Valkey: Buffer Operations > set and getBuffer with ArrayBuffer key
(skip) Valkey: Buffer Operations > set and getBuffer with Blob key
(pass) Valkey: Blob argument serialization > in-memory Blob arguments reach th
... (truncated)

release with fix: 9 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 665ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/124] cxx obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o
[2/124] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited
[2/124] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
   Compiling lol_html v2.7.2 (/workspace/bun/vendor/lolhtml)
   Compiling subtle v2.6.1
   Compiling arrayvec v0.7.6
   Compiling base64 v0.22.1
   Compiling constant_time_eq v0.4.2
   Compiling generic-array v0.14.7
   Compiling arrayref v0.3.9
   Compiling byteorder v1.5.0
   Compiling blake2b_simd v1.0.4
   Compiling crypto-common v0.1.7
   Compiling block-buffer v0.10.4
   Compiling inout v0.1.4

... (truncated)
diff hotspot
src/runtime/valkey_jsc/js_valkey_functions.rs |  11 ++-
 test/js/valkey/unit/buffer-operations.test.ts | 130 ++++++++++++++++++++++++++
 2 files changed, 140 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                           reads  edits  tests
src/runtime/valkey_jsc/js_valkey_functions.rs      4      3      0
test/js/valkey/unit/buffer-operations.test.ts      1      3      0