Skip to content

Commit b35dfd1

Browse files
committed
fs: aggregate errors in fsPromises to avoid error swallowing
Add AggregateError support to fsPromises, instead of swallowing errors if fs.close throws.
1 parent f9e07e4 commit b35dfd1

4 files changed

+225
-4
lines changed

lib/internal/fs/promises.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
PromisePrototypeFinally,
1111
PromisePrototypeThen,
1212
PromiseResolve,
13+
PromiseReject,
1314
SafeArrayIterator,
1415
Symbol,
1516
Uint8Array,
@@ -33,6 +34,7 @@ const {
3334
ERR_METHOD_NOT_IMPLEMENTED,
3435
},
3536
AbortError,
37+
aggregateTwoErrors,
3638
} = require('internal/errors');
3739
const { isArrayBufferView } = require('internal/util/types');
3840
const { rimrafPromises } = require('internal/fs/rimraf');
@@ -250,6 +252,19 @@ class FileHandle extends EventEmitterMixin(JSTransferable) {
250252
}
251253
}
252254

255+
async function handleFdClose(fileOpPromise, closeFunc) {
256+
return PromisePrototypeThen(
257+
fileOpPromise,
258+
(result) => PromisePrototypeThen(closeFunc(), () => result),
259+
(opError) =>
260+
PromisePrototypeThen(
261+
closeFunc(),
262+
() => PromiseReject(opError),
263+
(closeError) => PromiseReject(aggregateTwoErrors(closeError, opError))
264+
)
265+
);
266+
}
267+
253268
async function fsCall(fn, handle, ...args) {
254269
if (handle[kRefs] === undefined) {
255270
throw new ERR_INVALID_ARG_TYPE('filehandle', 'FileHandle', handle);
@@ -501,7 +516,7 @@ async function rename(oldPath, newPath) {
501516

502517
async function truncate(path, len = 0) {
503518
const fd = await open(path, 'r+');
504-
return PromisePrototypeFinally(ftruncate(fd, len), fd.close);
519+
return handleFdClose(ftruncate(fd, len), fd.close);
505520
}
506521

507522
async function ftruncate(handle, len = 0) {
@@ -632,7 +647,7 @@ async function lchmod(path, mode) {
632647
throw new ERR_METHOD_NOT_IMPLEMENTED('lchmod()');
633648

634649
const fd = await open(path, O_WRONLY | O_SYMLINK);
635-
return PromisePrototypeFinally(fchmod(fd, mode), fd.close);
650+
return handleFdClose(fchmod(fd, mode), fd.close);
636651
}
637652

638653
async function lchown(path, uid, gid) {
@@ -711,7 +726,7 @@ async function writeFile(path, data, options) {
711726
checkAborted(options.signal);
712727

713728
const fd = await open(path, flag, options.mode);
714-
return PromisePrototypeFinally(
729+
return handleFdClose(
715730
writeFileHandle(fd, data, options.signal, options.encoding), fd.close);
716731
}
717732

@@ -736,7 +751,7 @@ async function readFile(path, options) {
736751
checkAborted(options.signal);
737752

738753
const fd = await open(path, flag, 0o666);
739-
return PromisePrototypeFinally(readFileHandle(fd, options), fd.close);
754+
return handleFdClose(readFileHandle(fd, options), fd.close);
740755
}
741756

742757
module.exports = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
const common = require('../common');
5+
const tmpdir = require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
const fs = require('fs');
11+
const path = require('path');
12+
const {
13+
readFile,
14+
writeFile,
15+
truncate,
16+
lchmod,
17+
} = fs.promises;
18+
const {
19+
FileHandle,
20+
} = require('internal/fs/promises');
21+
22+
const assert = require('assert');
23+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
24+
25+
let count = 0;
26+
async function createFile() {
27+
const filePath = path.join(tmpdir.path, `aggregate_errors_${++count}.txt`);
28+
await writeFile(filePath, 'content');
29+
return filePath;
30+
}
31+
32+
async function checkAggregateError(op) {
33+
try {
34+
const filePath = await createFile();
35+
Object.defineProperty(FileHandle.prototype, 'fd', {
36+
get: function() {
37+
// Close is set by using a setter,
38+
// so it needs to be set on the instance.
39+
const originalClose = this.close;
40+
this.close = async () => {
41+
// close the file
42+
await originalClose.call(this);
43+
const closeError = new Error('CLOSE_ERROR');
44+
closeError.code = 456;
45+
throw closeError;
46+
};
47+
const opError = new Error('INTERNAL_ERROR');
48+
opError.code = 123;
49+
throw opError;
50+
}
51+
});
52+
53+
await op(filePath).catch(common.mustCall((err) => {
54+
assert.strictEqual(err.constructor.name, 'AggregateError');
55+
assert.strictEqual(err.code, 123);
56+
assert.strictEqual(err.errors.length, 2);
57+
assert.strictEqual(err.errors[0].message, 'INTERNAL_ERROR');
58+
assert.strictEqual(err.errors[1].message, 'CLOSE_ERROR');
59+
}));
60+
} finally {
61+
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
62+
}
63+
}
64+
(async function() {
65+
tmpdir.refresh();
66+
await checkAggregateError((filePath) => truncate(filePath));
67+
await checkAggregateError((filePath) => readFile(filePath));
68+
await checkAggregateError((filePath) => writeFile(filePath, '123'));
69+
if (common.isOSX) {
70+
await checkAggregateError((filePath) => lchmod(filePath, 0o777));
71+
}
72+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
const common = require('../common');
5+
const tmpdir = require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
const fs = require('fs');
11+
const path = require('path');
12+
const {
13+
readFile,
14+
writeFile,
15+
truncate,
16+
lchmod,
17+
} = fs.promises;
18+
const {
19+
FileHandle,
20+
} = require('internal/fs/promises');
21+
22+
const assert = require('assert');
23+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
24+
25+
let count = 0;
26+
async function createFile() {
27+
const filePath = path.join(tmpdir.path, `close_errors_${++count}.txt`);
28+
await writeFile(filePath, 'content');
29+
return filePath;
30+
}
31+
32+
async function checkAggregateError(op) {
33+
try {
34+
const filePath = await createFile();
35+
Object.defineProperty(FileHandle.prototype, 'fd', {
36+
get: function() {
37+
// Close is set by using a setter,
38+
// so it needs to be set on the instance.
39+
const originalClose = this.close;
40+
this.close = async () => {
41+
// close the file
42+
await originalClose.call(this);
43+
const closeError = new Error('CLOSE_ERROR');
44+
closeError.code = 456;
45+
throw closeError;
46+
};
47+
return originalFd.get.call(this);
48+
}
49+
});
50+
51+
await op(filePath).catch(common.mustCall((err) => {
52+
assert.strictEqual(err.constructor.name, 'Error');
53+
assert.strictEqual(err.message, 'CLOSE_ERROR');
54+
assert.strictEqual(err.code, 456);
55+
}));
56+
} finally {
57+
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
58+
}
59+
}
60+
(async function() {
61+
tmpdir.refresh();
62+
await checkAggregateError((filePath) => truncate(filePath));
63+
await checkAggregateError((filePath) => readFile(filePath));
64+
await checkAggregateError((filePath) => writeFile(filePath, '123'));
65+
if (common.isOSX) {
66+
await checkAggregateError((filePath) => lchmod(filePath, 0o777));
67+
}
68+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
const common = require('../common');
5+
const tmpdir = require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
const fs = require('fs');
11+
const path = require('path');
12+
const {
13+
readFile,
14+
writeFile,
15+
truncate,
16+
lchmod,
17+
} = fs.promises;
18+
const {
19+
FileHandle,
20+
} = require('internal/fs/promises');
21+
22+
const assert = require('assert');
23+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
24+
25+
let count = 0;
26+
async function createFile() {
27+
const filePath = path.join(tmpdir.path, `op_errors_${++count}.txt`);
28+
await writeFile(filePath, 'content');
29+
return filePath;
30+
}
31+
32+
async function checkAggregateError(op) {
33+
try {
34+
const filePath = await createFile();
35+
Object.defineProperty(FileHandle.prototype, 'fd', {
36+
get: function() {
37+
// Close is set by using a setter,
38+
// so it needs to be set on the instance.
39+
const originalClose = this.close;
40+
this.close = common.mustCall(function(...args) {
41+
return originalClose.apply(this, args);
42+
});
43+
const opError = new Error('INTERNAL_ERROR');
44+
opError.code = 123;
45+
throw opError;
46+
}
47+
});
48+
49+
await op(filePath).catch(common.mustCall((err) => {
50+
assert.strictEqual(err.constructor.name, 'Error');
51+
assert.strictEqual(err.message, 'INTERNAL_ERROR');
52+
assert.strictEqual(err.code, 123);
53+
}));
54+
} finally {
55+
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
56+
}
57+
}
58+
(async function() {
59+
tmpdir.refresh();
60+
await checkAggregateError((filePath) => truncate(filePath));
61+
await checkAggregateError((filePath) => readFile(filePath));
62+
await checkAggregateError((filePath) => writeFile(filePath, '123'));
63+
if (common.isOSX) {
64+
await checkAggregateError((filePath) => lchmod(filePath, 0o777));
65+
}
66+
})().then(common.mustCall());

0 commit comments

Comments
 (0)