Skip to content

Commit 5f88b64

Browse files
benjamingrtargos
authored andcommitted
fs: add support for AbortSignal in readFile
PR-URL: #35911 Backport-PR-URL: #38386 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent 88f4261 commit 5f88b64

File tree

7 files changed

+146
-14
lines changed

7 files changed

+146
-14
lines changed

doc/api/fs.md

+42
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,10 @@ If `options.withFileTypes` is set to `true`, the result will contain
30263026
<!-- YAML
30273027
added: v0.1.29
30283028
changes:
3029+
- version: REPLACEME
3030+
pr-url: https://github.com/nodejs/node/pull/35911
3031+
description: The options argument may include an AbortSignal to abort an
3032+
ongoing readFile request.
30293033
- version: v10.0.0
30303034
pr-url: https://github.com/nodejs/node/pull/12562
30313035
description: The `callback` parameter is no longer optional. Not passing
@@ -3051,6 +3055,7 @@ changes:
30513055
* `options` {Object|string}
30523056
* `encoding` {string|null} **Default:** `null`
30533057
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
3058+
* `signal` {AbortSignal} allows aborting an in-progress readFile
30543059
* `callback` {Function}
30553060
* `err` {Error}
30563061
* `data` {string|Buffer}
@@ -3092,9 +3097,25 @@ fs.readFile('<directory>', (err, data) => {
30923097
});
30933098
```
30943099

3100+
It is possible to abort an ongoing request using an `AbortSignal`. If a
3101+
request is aborted the callback is called with an `AbortError`:
3102+
3103+
```js
3104+
const controller = new AbortController();
3105+
const signal = controller.signal;
3106+
fs.readFile(fileInfo[0].name, { signal }, (err, buf) => {
3107+
// ...
3108+
});
3109+
// When you want to abort the request
3110+
controller.abort();
3111+
```
3112+
30953113
The `fs.readFile()` function buffers the entire file. To minimize memory costs,
30963114
when possible prefer streaming via `fs.createReadStream()`.
30973115

3116+
Aborting an ongoing request does not abort individual operating
3117+
system requests but rather the internal buffering `fs.readFile` performs.
3118+
30983119
### File descriptors
30993120

31003121
1. Any specified file descriptor has to support reading.
@@ -4748,6 +4769,7 @@ added: v10.0.0
47484769

47494770
* `options` {Object|string}
47504771
* `encoding` {string|null} **Default:** `null`
4772+
* `signal` {AbortSignal} allows aborting an in-progress readFile
47514773
* Returns: {Promise}
47524774

47534775
Asynchronously reads the entire contents of a file.
@@ -5411,12 +5433,18 @@ print('./').catch(console.error);
54115433
### `fsPromises.readFile(path[, options])`
54125434
<!-- YAML
54135435
added: v10.0.0
5436+
changes:
5437+
- version: REPLACEME
5438+
pr-url: https://github.com/nodejs/node/pull/35911
5439+
description: The options argument may include an AbortSignal to abort an
5440+
ongoing readFile request.
54145441
-->
54155442

54165443
* `path` {string|Buffer|URL|FileHandle} filename or `FileHandle`
54175444
* `options` {Object|string}
54185445
* `encoding` {string|null} **Default:** `null`
54195446
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
5447+
* `signal` {AbortSignal} allows aborting an in-progress readFile
54205448
* Returns: {Promise}
54215449

54225450
Asynchronously reads the entire contents of a file.
@@ -5432,6 +5460,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected
54325460
with an error. On FreeBSD, a representation of the directory's contents will be
54335461
returned.
54345462

5463+
It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a
5464+
request is aborted the promise returned is rejected with an `AbortError`:
5465+
5466+
```js
5467+
const controller = new AbortController();
5468+
const signal = controller.signal;
5469+
readFile(fileName, { signal }).then((file) => { /* ... */ });
5470+
// Abort the request
5471+
controller.abort();
5472+
```
5473+
5474+
Aborting an ongoing request does not abort individual operating
5475+
system requests but rather the internal buffering `fs.readFile` performs.
5476+
54355477
Any specified `FileHandle` has to support reading.
54365478

54375479
### `fsPromises.readlink(path[, options])`

lib/fs.js

+3
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ function readFile(path, options, callback) {
316316
const context = new ReadFileContext(callback, options.encoding);
317317
context.isUserFd = isFd(path); // File descriptor ownership
318318

319+
if (options.signal) {
320+
context.signal = options.signal;
321+
}
319322
if (context.isUserFd) {
320323
process.nextTick(function tick(context) {
321324
readFileAfterOpen.call({ context }, null, path);

lib/internal/fs/promises.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ const {
3030
} = internalBinding('constants').fs;
3131
const binding = internalBinding('fs');
3232
const { Buffer } = require('buffer');
33+
34+
const { codes, hideStackFrames } = require('internal/errors');
3335
const {
3436
ERR_FS_FILE_TOO_LARGE,
3537
ERR_INVALID_ARG_TYPE,
3638
ERR_INVALID_ARG_VALUE,
37-
ERR_METHOD_NOT_IMPLEMENTED
38-
} = require('internal/errors').codes;
39+
ERR_METHOD_NOT_IMPLEMENTED,
40+
} = codes;
3941
const { isArrayBufferView } = require('internal/util/types');
4042
const { rimrafPromises } = require('internal/fs/rimraf');
4143
const {
@@ -83,6 +85,13 @@ const {
8385
const getDirectoryEntriesPromise = promisify(getDirents);
8486
const validateRmOptionsPromise = promisify(validateRmOptions);
8587

88+
let DOMException;
89+
const lazyDOMException = hideStackFrames((message, name) => {
90+
if (DOMException === undefined)
91+
DOMException = internalBinding('messaging').DOMException;
92+
return new DOMException(message, name);
93+
});
94+
8695
class FileHandle extends JSTransferable {
8796
constructor(filehandle) {
8897
super();
@@ -260,8 +269,17 @@ async function writeFileHandle(filehandle, data) {
260269
}
261270

262271
async function readFileHandle(filehandle, options) {
272+
const signal = options && options.signal;
273+
274+
if (signal && signal.aborted) {
275+
throw lazyDOMException('The operation was aborted', 'AbortError');
276+
}
263277
const statFields = await binding.fstat(filehandle.fd, false, kUsePromises);
264278

279+
if (signal && signal.aborted) {
280+
throw lazyDOMException('The operation was aborted', 'AbortError');
281+
}
282+
265283
let size;
266284
if ((statFields[1/* mode */] & S_IFMT) === S_IFREG) {
267285
size = statFields[8/* size */];
@@ -278,6 +296,9 @@ async function readFileHandle(filehandle, options) {
278296
MathMin(size, kReadFileMaxChunkSize);
279297
let endOfFile = false;
280298
do {
299+
if (signal && signal.aborted) {
300+
throw lazyDOMException('The operation was aborted', 'AbortError');
301+
}
281302
const buf = Buffer.alloc(chunkSize);
282303
const { bytesRead, buffer } =
283304
await read(filehandle, buf, 0, chunkSize, -1);

lib/internal/fs/read_file_context.js

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ const { Buffer } = require('buffer');
88

99
const { FSReqCallback, close, read } = internalBinding('fs');
1010

11+
const { hideStackFrames } = require('internal/errors');
12+
13+
14+
let DOMException;
15+
const lazyDOMException = hideStackFrames((message, name) => {
16+
if (DOMException === undefined)
17+
DOMException = internalBinding('messaging').DOMException;
18+
return new DOMException(message, name);
19+
});
20+
1121
// Use 64kb in case the file type is not a regular file and thus do not know the
1222
// actual file size. Increasing the value further results in more frequent over
1323
// allocation for small files and consumes CPU time and memory that should be
@@ -74,13 +84,19 @@ class ReadFileContext {
7484
this.pos = 0;
7585
this.encoding = encoding;
7686
this.err = null;
87+
this.signal = undefined;
7788
}
7889

7990
read() {
8091
let buffer;
8192
let offset;
8293
let length;
8394

95+
if (this.signal && this.signal.aborted) {
96+
return this.close(
97+
lazyDOMException('The operation was aborted', 'AbortError')
98+
);
99+
}
84100
if (this.size === 0) {
85101
buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength);
86102
offset = 0;

lib/internal/fs/utils.js

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const {
3737
const { once } = require('internal/util');
3838
const { toPathIfFileURL } = require('internal/url');
3939
const {
40+
validateAbortSignal,
4041
validateBoolean,
4142
validateInt32,
4243
validateUint32
@@ -297,6 +298,10 @@ function getOptions(options, defaultOptions) {
297298

298299
if (options.encoding !== 'buffer')
299300
assertEncoding(options.encoding);
301+
302+
if (options.signal !== undefined) {
303+
validateAbortSignal(options.signal, 'options.signal');
304+
}
300305
return options;
301306
}
302307

test/parallel/test-fs-promises-readfile.js

+38-12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23

34
const common = require('../common');
@@ -10,18 +11,21 @@ tmpdir.refresh();
1011

1112
const fn = path.join(tmpdir.path, 'large-file');
1213

13-
async function validateReadFile() {
14-
// Creating large buffer with random content
15-
const buffer = Buffer.from(
16-
Array.apply(null, { length: 16834 * 2 })
17-
.map(Math.random)
18-
.map((number) => (number * (1 << 8)))
19-
);
14+
// Creating large buffer with random content
15+
const largeBuffer = Buffer.from(
16+
Array.apply(null, { length: 16834 * 2 })
17+
.map(Math.random)
18+
.map((number) => (number * (1 << 8)))
19+
);
2020

21+
async function createLargeFile() {
2122
// Writing buffer to a file then try to read it
22-
await writeFile(fn, buffer);
23+
await writeFile(fn, largeBuffer);
24+
}
25+
26+
async function validateReadFile() {
2327
const readBuffer = await readFile(fn);
24-
assert.strictEqual(readBuffer.equals(buffer), true);
28+
assert.strictEqual(readBuffer.equals(largeBuffer), true);
2529
}
2630

2731
async function validateReadFileProc() {
@@ -39,6 +43,28 @@ async function validateReadFileProc() {
3943
assert.ok(hostname.length > 0);
4044
}
4145

42-
validateReadFile()
43-
.then(() => validateReadFileProc())
44-
.then(common.mustCall());
46+
function validateReadFileAbortLogicBefore() {
47+
const controller = new AbortController();
48+
const signal = controller.signal;
49+
controller.abort();
50+
assert.rejects(readFile(fn, { signal }), {
51+
name: 'AbortError'
52+
});
53+
}
54+
55+
function validateReadFileAbortLogicDuring() {
56+
const controller = new AbortController();
57+
const signal = controller.signal;
58+
process.nextTick(() => controller.abort());
59+
assert.rejects(readFile(fn, { signal }), {
60+
name: 'AbortError'
61+
});
62+
}
63+
64+
(async () => {
65+
await createLargeFile();
66+
await validateReadFile();
67+
await validateReadFileProc();
68+
await validateReadFileAbortLogicBefore();
69+
await validateReadFileAbortLogicDuring();
70+
})().then(common.mustCall());

test/parallel/test-fs-readfile.js

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23
const common = require('../common');
34

@@ -57,3 +58,21 @@ for (const e of fileInfo) {
5758
assert.deepStrictEqual(buf, e.contents);
5859
}));
5960
}
61+
{
62+
// Test cancellation, before
63+
const controller = new AbortController();
64+
const signal = controller.signal;
65+
controller.abort();
66+
fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => {
67+
assert.strictEqual(err.name, 'AbortError');
68+
}));
69+
}
70+
{
71+
// Test cancellation, during read
72+
const controller = new AbortController();
73+
const signal = controller.signal;
74+
fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => {
75+
assert.strictEqual(err.name, 'AbortError');
76+
}));
77+
process.nextTick(() => controller.abort());
78+
}

0 commit comments

Comments
 (0)