Skip to content

Commit f27b7cf

Browse files
Linkgoronjasnell
authored andcommitted
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

Lines changed: 219 additions & 4 deletions

File tree

‎lib/internal/fs/promises.js‎

Lines changed: 19 additions & 4 deletions
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+
asyncfunctionhandleFdClose(fileOpPromise,closeFunc){
256+
returnPromisePrototypeThen(
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
asyncfunctionfsCall(fn,handle, ...args){
254269
if(handle[kRefs]===undefined){
255270
thrownewERR_INVALID_ARG_TYPE('filehandle','FileHandle',handle);
@@ -501,7 +516,7 @@ async function rename(oldPath, newPath) {
501516

502517
asyncfunctiontruncate(path,len=0){
503518
constfd=awaitopen(path,'r+');
504-
returnPromisePrototypeFinally(ftruncate(fd,len),fd.close);
519+
returnhandleFdClose(ftruncate(fd,len),fd.close);
505520
}
506521

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

634649
constfd=awaitopen(path,O_WRONLY|O_SYMLINK);
635-
returnPromisePrototypeFinally(fchmod(fd,mode),fd.close);
650+
returnhandleFdClose(fchmod(fd,mode),fd.close);
636651
}
637652

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

713728
constfd=awaitopen(path,flag,options.mode);
714-
returnPromisePrototypeFinally(
729+
returnhandleFdClose(
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
constfd=awaitopen(path,flag,0o666);
739-
returnPromisePrototypeFinally(readFileHandle(fd,options),fd.close);
754+
returnhandleFdClose(readFileHandle(fd,options),fd.close);
740755
}
741756

742757
module.exports={
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
constcommon=require('../common');
5+
consttmpdir=require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
constpath=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+
constassert=require('assert');
22+
constoriginalFd=Object.getOwnPropertyDescriptor(FileHandle.prototype,'fd');
23+
24+
letcount=0;
25+
asyncfunctioncreateFile(){
26+
constfilePath=path.join(tmpdir.path,`aggregate_errors_${++count}.txt`);
27+
awaitwriteFile(filePath,'content');
28+
returnfilePath;
29+
}
30+
31+
asyncfunctioncheckAggregateError(op){
32+
try{
33+
constfilePath=awaitcreateFile();
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+
constoriginalClose=this.close;
39+
this.close=async()=>{
40+
// close the file
41+
awaitoriginalClose.call(this);
42+
constcloseError=newError('CLOSE_ERROR');
43+
closeError.code=456;
44+
throwcloseError;
45+
};
46+
constopError=newError('INTERNAL_ERROR');
47+
opError.code=123;
48+
throwopError;
49+
}
50+
});
51+
52+
awaitassert.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+
returntrue;
59+
}));
60+
}finally{
61+
Object.defineProperty(FileHandle.prototype,'fd',originalFd);
62+
}
63+
}
64+
(asyncfunction(){
65+
tmpdir.refresh();
66+
awaitcheckAggregateError((filePath)=>truncate(filePath));
67+
awaitcheckAggregateError((filePath)=>readFile(filePath));
68+
awaitcheckAggregateError((filePath)=>writeFile(filePath,'123'));
69+
if(common.isOSX){
70+
awaitcheckAggregateError((filePath)=>lchmod(filePath,0o777));
71+
}
72+
})().then(common.mustCall());
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
constcommon=require('../common');
5+
consttmpdir=require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
constpath=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+
constassert=require('assert');
22+
constoriginalFd=Object.getOwnPropertyDescriptor(FileHandle.prototype,'fd');
23+
24+
letcount=0;
25+
asyncfunctioncreateFile(){
26+
constfilePath=path.join(tmpdir.path,`close_errors_${++count}.txt`);
27+
awaitwriteFile(filePath,'content');
28+
returnfilePath;
29+
}
30+
31+
asyncfunctioncheckCloseError(op){
32+
try{
33+
constfilePath=awaitcreateFile();
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+
constoriginalClose=this.close;
39+
this.close=async()=>{
40+
// close the file
41+
awaitoriginalClose.call(this);
42+
constcloseError=newError('CLOSE_ERROR');
43+
closeError.code=456;
44+
throwcloseError;
45+
};
46+
returnoriginalFd.get.call(this);
47+
}
48+
});
49+
50+
awaitassert.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+
(asyncfunction(){
60+
tmpdir.refresh();
61+
awaitcheckCloseError((filePath)=>truncate(filePath));
62+
awaitcheckCloseError((filePath)=>readFile(filePath));
63+
awaitcheckCloseError((filePath)=>writeFile(filePath,'123'));
64+
if(common.isOSX){
65+
awaitcheckCloseError((filePath)=>lchmod(filePath,0o777));
66+
}
67+
})().then(common.mustCall());
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
// Flags: --expose-internals
3+
4+
constcommon=require('../common');
5+
consttmpdir=require('../common/tmpdir');
6+
7+
// The following tests validate aggregate errors are thrown correctly
8+
// when both an operation and close throw.
9+
10+
constpath=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+
constassert=require('assert');
22+
constoriginalFd=Object.getOwnPropertyDescriptor(FileHandle.prototype,'fd');
23+
24+
letcount=0;
25+
asyncfunctioncreateFile(){
26+
constfilePath=path.join(tmpdir.path,`op_errors_${++count}.txt`);
27+
awaitwriteFile(filePath,'content');
28+
returnfilePath;
29+
}
30+
31+
asyncfunctioncheckOperationError(op){
32+
try{
33+
constfilePath=awaitcreateFile();
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+
constopError=newError('INTERNAL_ERROR');
39+
opError.code=123;
40+
throwopError;
41+
}
42+
});
43+
44+
awaitassert.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+
(asyncfunction(){
54+
tmpdir.refresh();
55+
awaitcheckOperationError((filePath)=>truncate(filePath));
56+
awaitcheckOperationError((filePath)=>readFile(filePath));
57+
awaitcheckOperationError((filePath)=>writeFile(filePath,'123'));
58+
if(common.isOSX){
59+
awaitcheckOperationError((filePath)=>lchmod(filePath,0o777));
60+
}
61+
})().then(common.mustCall());

0 commit comments

Comments
 (0)