From 9f85ef528ce1c993657fcc102135373095961bf6 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Mon, 29 Jun 2026 22:57:56 -0400 Subject: [PATCH] Throw EEXIST when ensureSymlink dest is a broken symlink ensureSymlink/ensureSymlinkSync stat() the destination to check whether an existing symlink already points at srcpath. When that symlink is broken, following it fails and the raw ENOENT (stat '') surfaced to the caller, which is confusing: a symlink that points at a *different* existing target already throws EEXIST. Skip the idempotency check when the destination can't be followed so the existing creation path reports EEXIST consistently, broken or not. Closes #925 --- lib/ensure/__tests__/symlink.test.js | 23 +++++++++++++++++++++++ lib/ensure/symlink.js | 18 ++++++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/ensure/__tests__/symlink.test.js b/lib/ensure/__tests__/symlink.test.js index cec7e9dd..e0850065 100644 --- a/lib/ensure/__tests__/symlink.test.js +++ b/lib/ensure/__tests__/symlink.test.js @@ -399,4 +399,27 @@ describe('fse-ensure-symlink', () => { assert.strictEqual(content, 'content') }) }) + + // https://github.com/jprichardson/node-fs-extra/issues/925 + describe('ensureSymlink() when dest is an existing broken symlink (issue #925)', () => { + it('should fail with EEXIST (not ENOENT) when dest is a broken symlink', async () => { + const srcpath = path.join(TEST_DIR, 'src-925.txt') + const dstpath = path.join(TEST_DIR, 'broken-link-925') + fs.writeFileSync(srcpath, 'src\n') + fs.symlinkSync(path.join(TEST_DIR, 'missing-925.txt'), dstpath) + assert.strictEqual(fs.lstatSync(dstpath).isSymbolicLink(), true) + + await assert.rejects(ensureSymlink(srcpath, dstpath), { code: 'EEXIST' }) + }) + + it('should fail with EEXIST (not ENOENT) when dest is a broken symlink (sync)', () => { + const srcpath = path.join(TEST_DIR, 'src-925-sync.txt') + const dstpath = path.join(TEST_DIR, 'broken-link-925-sync') + fs.writeFileSync(srcpath, 'src\n') + fs.symlinkSync(path.join(TEST_DIR, 'missing-925-sync.txt'), dstpath) + assert.strictEqual(fs.lstatSync(dstpath).isSymbolicLink(), true) + + assert.throws(() => ensureSymlinkSync(srcpath, dstpath), { code: 'EEXIST' }) + }) + }) }) diff --git a/lib/ensure/symlink.js b/lib/ensure/symlink.js index a7de8cad..f283672e 100644 --- a/lib/ensure/symlink.js +++ b/lib/ensure/symlink.js @@ -35,8 +35,13 @@ async function createSymlink (srcpath, dstpath, type) { } } - const dstStat = await fs.stat(dstpath, { bigint: true }) - if (areIdentical(srcStat, dstStat)) return + // Following the existing symlink fails if it's broken; in that case fall + // through so fs.symlink reports EEXIST, rather than leaking the stat ENOENT. + let dstStat + try { + dstStat = await fs.stat(dstpath, { bigint: true }) + } catch { } + if (dstStat && areIdentical(srcStat, dstStat)) return } const relative = await symlinkPaths(srcpath, dstpath) @@ -72,8 +77,13 @@ function createSymlinkSync (srcpath, dstpath, type) { } } - const dstStat = fs.statSync(dstpath, { bigint: true }) - if (areIdentical(srcStat, dstStat)) return + // Following the existing symlink fails if it's broken; in that case fall + // through so fs.symlinkSync reports EEXIST, rather than leaking the stat ENOENT. + let dstStat + try { + dstStat = fs.statSync(dstpath, { bigint: true }) + } catch { } + if (dstStat && areIdentical(srcStat, dstStat)) return } const relative = symlinkPathsSync(srcpath, dstpath)