From 0e065fe12235481a595667e5610a7191380b9484 Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Mon, 24 Aug 2026 10:52:28 -0400 Subject: [PATCH] src: report libuv error when openAsBlob cannot stat `FdEntry::Create()` returned nullptr for any failed `uv_fs_stat()`, discarding the status, and `BlobFromFilePath()` turned that into `ERR_INVALID_ARG_VALUE: Unable to open file as blob`. A missing file is not a malformed argument, and the resulting `TypeError` carried no `errno`, `syscall`, or `path`, so ENOENT could not be told apart from any other reason the path was unusable. Thread the libuv status out of `CreateFdEntry()` and throw a `UVException` instead, so `fs.openAsBlob()` reports the same error `fs.stat()` does for the same path. Fixes: https://github.com/nodejs/node/issues/65514 Signed-off-by: Paul Bouchon --- src/dataqueue/queue.cc | 15 +++++++++++---- src/dataqueue/queue.h | 5 ++++- src/node_blob.cc | 7 +++++-- test/parallel/test-blob-file-backed.js | 13 +++++++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/dataqueue/queue.cc b/src/dataqueue/queue.cc index 8516362b7a87..bddf840942e2 100644 --- a/src/dataqueue/queue.cc +++ b/src/dataqueue/queue.cc @@ -847,13 +847,19 @@ class FdEntry final : public EntryImpl { // the race // condition described in the comment above. public: - static std::unique_ptr Create(Environment* env, Local path) { + static std::unique_ptr Create(Environment* env, + Local path, + int* status) { // We're only going to create the FdEntry if the file exists. uv_fs_t req = uv_fs_t(); auto cleanup = OnScopeLeave([&] { uv_fs_req_cleanup(&req); }); auto buf = std::make_shared(env->isolate(), path); - if (uv_fs_stat(nullptr, &req, buf->out(), nullptr) < 0) return nullptr; + int err = uv_fs_stat(nullptr, &req, buf->out(), nullptr); + if (err < 0) { + if (status != nullptr) *status = err; + return nullptr; + } return std::make_unique( env, std::move(buf), req.statbuf, 0, req.statbuf.st_size); @@ -1162,8 +1168,9 @@ std::unique_ptr DataQueue::CreateDataQueueEntry( } std::unique_ptr DataQueue::CreateFdEntry(Environment* env, - Local path) { - return FdEntry::Create(env, path); + Local path, + int* status) { + return FdEntry::Create(env, path, status); } void DataQueue::Initialize(Environment* env, v8::Local target) { diff --git a/src/dataqueue/queue.h b/src/dataqueue/queue.h index 190c54111b3e..64c7404e8b41 100644 --- a/src/dataqueue/queue.h +++ b/src/dataqueue/queue.h @@ -226,8 +226,11 @@ class DataQueue : public MemoryRetainer { static std::unique_ptr CreateDataQueueEntry( std::shared_ptr data_queue); + // Returns nullptr if the file cannot be stat'd. When `status` is given, it + // is set to the libuv error code so callers can report why. static std::unique_ptr CreateFdEntry(Environment* env, - v8::Local path); + v8::Local path, + int* status = nullptr); // Creates a Reader for the given queue. If the queue is idempotent, // any number of readers can be created, all of which are guaranteed diff --git a/src/node_blob.cc b/src/node_blob.cc index 57d35358fbfd..b046f67f1691 100644 --- a/src/node_blob.cc +++ b/src/node_blob.cc @@ -110,9 +110,12 @@ void BlobFromFilePath(const FunctionCallbackInfo& args) { ToNamespacedPath(env, &path); THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kFileSystemRead, path.ToStringView()); - auto entry = DataQueue::CreateFdEntry(env, args[0]); + int status = 0; + auto entry = DataQueue::CreateFdEntry(env, args[0], &status); if (entry == nullptr) { - return THROW_ERR_INVALID_ARG_VALUE(env, "Unable to open file as blob"); + // The file could not be stat'd. Report the libuv error so callers can tell + // ENOENT apart from any other reason the path could not be used. + return env->ThrowUVException(status, "stat", nullptr, *path); } std::vector> entries; diff --git a/test/parallel/test-blob-file-backed.js b/test/parallel/test-blob-file-backed.js index f94eae6d6ed7..c5a5ce14807d 100644 --- a/test/parallel/test-blob-file-backed.js +++ b/test/parallel/test-blob-file-backed.js @@ -129,6 +129,19 @@ writeFileSync(testfile5, ''); await unlink(testfile5); })().then(common.mustCall()); +(async () => { + // A path that cannot be stat'd reports the underlying libuv error rather + // than a generic argument error, so ENOENT can be told apart from any other + // reason the file could not be used. + // Refs: https://github.com/nodejs/node/issues/65514 + const missing = tmpdir.resolve('does-not-exist.txt'); + await assert.rejects(async () => openAsBlob(missing), { + code: 'ENOENT', + syscall: 'stat', + path: missing, + }); +})().then(common.mustCall()); + (async () => { // We currently do not allow File-backed blobs to be cloned or transferred // across worker threads. This is largely because the underlying FdEntry