Skip to content

Commit 92bdfd1

Browse files
benjamingrdanielleadams
authored andcommitted
fs: add support for AbortSignal in readFile
PR-URL: #35911 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 d83e253 commit 92bdfd1

File tree

7 files changed

+144
-14
lines changed

7 files changed

+144
-14
lines changed

doc/api/fs.md

+42
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,10 @@ If `options.withFileTypes` is set to `true`, the result will contain
30313031
<!-- YAML
30323032
added: v0.1.29
30333033
changes:
3034+
- version: REPLACEME
3035+
pr-url: https://github.com/nodejs/node/pull/35911
3036+
description: The options argument may include an AbortSignal to abort an
3037+
ongoing readFile request.
30343038
- version: v10.0.0
30353039
pr-url: https://github.com/nodejs/node/pull/12562
30363040
description: The `callback` parameter is no longer optional. Not passing
@@ -3056,6 +3060,7 @@ changes:
30563060
* `options` {Object|string}
30573061
* `encoding` {string|null} **Default:** `null`
30583062
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
3063+
* `signal` {AbortSignal} allows aborting an in-progress readFile
30593064
* `callback` {Function}
30603065
* `err` {Error}
30613066
* `data` {string|Buffer}
@@ -3097,9 +3102,25 @@ fs.readFile('<directory>', (err, data) => {
30973102
});
30983103
```
30993104

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

3121+
Aborting an ongoing request does not abort individual operating
3122+
system requests but rather the internal buffering `fs.readFile` performs.
3123+
31033124
### File descriptors
31043125

31053126
1. Any specified file descriptor has to support reading.
@@ -4771,6 +4792,7 @@ added: v10.0.0
47714792

47724793
* `options` {Object|string}
47734794
* `encoding` {string|null} **Default:** `null`
4795+
* `signal` {AbortSignal} allows aborting an in-progress readFile
47744796
* Returns: {Promise}
47754797

47764798
Asynchronously reads the entire contents of a file.
@@ -5438,12 +5460,18 @@ print('./').catch(console.error);
54385460
### `fsPromises.readFile(path[, options])`
54395461
<!-- YAML
54405462
added: v10.0.0
5463+
changes:
5464+
- version: REPLACEME
5465+
pr-url: https://github.com/nodejs/node/pull/35911
5466+
description: The options argument may include an AbortSignal to abort an
5467+
ongoing readFile request.
54415468
-->
54425469

54435470
* `path` {string|Buffer|URL|FileHandle} filename or `FileHandle`
54445471
* `options` {Object|string}
54455472
* `encoding` {string|null} **Default:** `null`
54465473
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
5474+
* `signal` {AbortSignal} allows aborting an in-progress readFile
54475475
* Returns: {Promise}
54485476

54495477
Asynchronously reads the entire contents of a file.
@@ -5459,6 +5487,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected
54595487
with an error. On FreeBSD, a representation of the directory's contents will be
54605488
returned.
54615489

5490+
It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a
5491+
request is aborted the promise returned is rejected with an `AbortError`:
5492+
5493+
```js
5494+
const controller = new AbortController();
5495+
const signal = controller.signal;
5496+
readFile(fileName, { signal }).then((file) => { /* ... */ });
5497+
// Abort the request
5498+
controller.abort();
5499+
```
5500+
5501+
Aborting an ongoing request does not abort individual operating
5502+
system requests but rather the internal buffering `fs.readFile` performs.
5503+
54625504
Any specified `FileHandle` has to support reading.
54635505

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

lib/fs.js

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

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

lib/internal/fs/promises.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ const {
2929
} = internalBinding('constants').fs;
3030
const binding = internalBinding('fs');
3131
const { Buffer } = require('buffer');
32+
33+
const { codes, hideStackFrames } = require('internal/errors');
3234
const {
3335
ERR_FS_FILE_TOO_LARGE,
3436
ERR_INVALID_ARG_TYPE,
3537
ERR_INVALID_ARG_VALUE,
36-
ERR_METHOD_NOT_IMPLEMENTED
37-
} = require('internal/errors').codes;
38+
ERR_METHOD_NOT_IMPLEMENTED,
39+
} = codes;
3840
const { isArrayBufferView } = require('internal/util/types');
3941
const { rimrafPromises } = require('internal/fs/rimraf');
4042
const {
@@ -82,6 +84,13 @@ const {
8284
const getDirectoryEntriesPromise = promisify(getDirents);
8385
const validateRmOptionsPromise = promisify(validateRmOptions);
8486

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

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

278+
if (signal && signal.aborted) {
279+
throw lazyDOMException('The operation was aborted', 'AbortError');
280+
}
281+
264282
let size;
265283
if ((statFields[1/* mode */] & S_IFMT) === S_IFREG) {
266284
size = statFields[8/* size */];
@@ -277,6 +295,9 @@ async function readFileHandle(filehandle, options) {
277295
MathMin(size, kReadFileMaxChunkSize);
278296
let endOfFile = false;
279297
do {
298+
if (signal && signal.aborted) {
299+
throw lazyDOMException('The operation was aborted', 'AbortError');
300+
}
280301
const buf = Buffer.alloc(chunkSize);
281302
const { bytesRead, buffer } =
282303
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
@@ -35,6 +35,7 @@ const {
3535
const { once } = require('internal/util');
3636
const { toPathIfFileURL } = require('internal/url');
3737
const {
38+
validateAbortSignal,
3839
validateBoolean,
3940
validateInt32,
4041
validateUint32
@@ -296,6 +297,10 @@ function getOptions(options, defaultOptions) {
296297

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

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

+37-12
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@ tmpdir.refresh();
1010

1111
const fn = path.join(tmpdir.path, 'large-file');
1212

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-
);
13+
// Creating large buffer with random content
14+
const largeBuffer = Buffer.from(
15+
Array.apply(null, { length: 16834 * 2 })
16+
.map(Math.random)
17+
.map((number) => (number * (1 << 8)))
18+
);
2019

20+
async function createLargeFile() {
2121
// Writing buffer to a file then try to read it
22-
await writeFile(fn, buffer);
22+
await writeFile(fn, largeBuffer);
23+
}
24+
25+
async function validateReadFile() {
2326
const readBuffer = await readFile(fn);
24-
assert.strictEqual(readBuffer.equals(buffer), true);
27+
assert.strictEqual(readBuffer.equals(largeBuffer), true);
2528
}
2629

2730
async function validateReadFileProc() {
@@ -39,6 +42,28 @@ async function validateReadFileProc() {
3942
assert.ok(hostname.length > 0);
4043
}
4144

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

test/parallel/test-fs-readfile.js

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

0 commit comments

Comments
 (0)