Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f27b7cf

Browse files
Linkgoronjasnell
authored andcommittedApr 26, 2021
fs: aggregate errors in fsPromises to avoid error swallowing
Add AggregateError support to fsPromises, instead of swallowing errors if fs.close throws. PR-URL: #38259 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Darshan Sen <raisinten@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 91d1b60 commit f27b7cf

4 files changed

+219
-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 path = require('path');
11+
const {
12+
readFile,
13+
writeFile,
14+
truncate,
15+
lchmod,
16+
} = require('fs/promises');
17+
const {
18+
FileHandle,
19+
} = require('internal/fs/promises');
20+
21+
const assert = require('assert');
22+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
23+
24+
let count = 0;
25+
async function createFile() {
26+
const filePath = path.join(tmpdir.path, `aggregate_errors_${++count}.txt`);
27+
await writeFile(filePath, 'content');
28+
return filePath;
29+
}
30+
31+
async function checkAggregateError(op) {
32+
try {
33+
const filePath = await createFile();
34+
Object.defineProperty(FileHandle.prototype, 'fd', {
35+
get: function() {
36+
// Close is set by using a setter,
37+
// so it needs to be set on the instance.
38+
const originalClose = this.close;
39+
this.close = async () => {
40+
// close the file
41+
await originalClose.call(this);
42+
const closeError = new Error('CLOSE_ERROR');
43+
closeError.code = 456;
44+
throw closeError;
45+
};
46+
const opError = new Error('INTERNAL_ERROR');
47+
opError.code = 123;
48+
throw opError;
49+
}
50+
});
51+
52+
await assert.rejects(op(filePath), common.mustCall((err) => {
53+
assert.strictEqual(err.name, 'AggregateError');
54+
assert.strictEqual(err.code, 123);
55+
assert.strictEqual(err.errors.length, 2);
56+
assert.strictEqual(err.errors[0].message, 'INTERNAL_ERROR');
57+
assert.strictEqual(err.errors[1].message, 'CLOSE_ERROR');
58+
return true;
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,67 @@
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 path = require('path');
11+
const {
12+
readFile,
13+
writeFile,
14+
truncate,
15+
lchmod,
16+
} = require('fs/promises');
17+
const {
18+
FileHandle,
19+
} = require('internal/fs/promises');
20+
21+
const assert = require('assert');
22+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
23+
24+
let count = 0;
25+
async function createFile() {
26+
const filePath = path.join(tmpdir.path, `close_errors_${++count}.txt`);
27+
await writeFile(filePath, 'content');
28+
return filePath;
29+
}
30+
31+
async function checkCloseError(op) {
32+
try {
33+
const filePath = await createFile();
34+
Object.defineProperty(FileHandle.prototype, 'fd', {
35+
get: function() {
36+
// Close is set by using a setter,
37+
// so it needs to be set on the instance.
38+
const originalClose = this.close;
39+
this.close = async () => {
40+
// close the file
41+
await originalClose.call(this);
42+
const closeError = new Error('CLOSE_ERROR');
43+
closeError.code = 456;
44+
throw closeError;
45+
};
46+
return originalFd.get.call(this);
47+
}
48+
});
49+
50+
await assert.rejects(op(filePath), {
51+
name: 'Error',
52+
message: 'CLOSE_ERROR',
53+
code: 456,
54+
});
55+
} finally {
56+
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
57+
}
58+
}
59+
(async function() {
60+
tmpdir.refresh();
61+
await checkCloseError((filePath) => truncate(filePath));
62+
await checkCloseError((filePath) => readFile(filePath));
63+
await checkCloseError((filePath) => writeFile(filePath, '123'));
64+
if (common.isOSX) {
65+
await checkCloseError((filePath) => lchmod(filePath, 0o777));
66+
}
67+
})().then(common.mustCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 path = require('path');
11+
const {
12+
readFile,
13+
writeFile,
14+
truncate,
15+
lchmod,
16+
} = require('fs/promises');
17+
const {
18+
FileHandle,
19+
} = require('internal/fs/promises');
20+
21+
const assert = require('assert');
22+
const originalFd = Object.getOwnPropertyDescriptor(FileHandle.prototype, 'fd');
23+
24+
let count = 0;
25+
async function createFile() {
26+
const filePath = path.join(tmpdir.path, `op_errors_${++count}.txt`);
27+
await writeFile(filePath, 'content');
28+
return filePath;
29+
}
30+
31+
async function checkOperationError(op) {
32+
try {
33+
const filePath = await createFile();
34+
Object.defineProperty(FileHandle.prototype, 'fd', {
35+
get: function() {
36+
// Verify that close is called when an error is thrown
37+
this.close = common.mustCall(this.close);
38+
const opError = new Error('INTERNAL_ERROR');
39+
opError.code = 123;
40+
throw opError;
41+
}
42+
});
43+
44+
await assert.rejects(op(filePath), {
45+
name: 'Error',
46+
message: 'INTERNAL_ERROR',
47+
code: 123,
48+
});
49+
} finally {
50+
Object.defineProperty(FileHandle.prototype, 'fd', originalFd);
51+
}
52+
}
53+
(async function() {
54+
tmpdir.refresh();
55+
await checkOperationError((filePath) => truncate(filePath));
56+
await checkOperationError((filePath) => readFile(filePath));
57+
await checkOperationError((filePath) => writeFile(filePath, '123'));
58+
if (common.isOSX) {
59+
await checkOperationError((filePath) => lchmod(filePath, 0o777));
60+
}
61+
})().then(common.mustCall());

0 commit comments

Comments
 (0)
Please sign in to comment.