Skip to content

Commit eaddb03

Browse files
committed
stream: destroying stream without error is abort
If an autoDestroy stream is destroyed by user without an error we automatically convert it to an AbortError in order to avoid a weird state.
1 parent f217025 commit eaddb03

File tree

6 files changed

+61
-1
lines changed

6 files changed

+61
-1
lines changed

doc/api/stream.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ This is a destructive and immediate way to destroy a stream. Previous calls to
421421
Use `end()` instead of destroy if data should flush before close, or wait for
422422
the `'drain'` event before destroying the stream.
423423

424+
If `.destroy()` is called without an `error` and `autoDestroy` is
425+
enabled, then if the stream has not completed it will be
426+
automatically destroyed with an `AbortError`.
427+
424428
```cjs
425429
const { Writable } = require('stream');
426430

@@ -1101,6 +1105,10 @@ further errors except from `_destroy()` may be emitted as `'error'`.
11011105
Implementors should not override this method, but instead implement
11021106
[`readable._destroy()`][readable-_destroy].
11031107

1108+
If `.destroy()` is called without an `error` and `autoDestroy` is
1109+
enabled, then if the stream has not completed it will be
1110+
automatically destroyed with an `AbortError`.
1111+
11041112
##### `readable.closed`
11051113

11061114
<!-- YAML
@@ -1805,6 +1813,10 @@ unless `emitClose` is set in false.
18051813
Once `destroy()` has been called, any further calls will be a no-op and no
18061814
further errors except from `_destroy()` may be emitted as `'error'`.
18071815

1816+
If `.destroy()` is called without an `error` and `autoDestroy` is
1817+
enabled, then if the stream has not completed it will be
1818+
automatically destroyed with an `AbortError`.
1819+
18081820
### `stream.finished(stream[, options], callback)`
18091821

18101822
<!-- YAML
@@ -2508,6 +2520,8 @@ changes:
25082520
[`stream._construct()`][writable-_construct] method.
25092521
* `autoDestroy` {boolean} Whether this stream should automatically call
25102522
`.destroy()` on itself after ending. **Default:** `true`.
2523+
* `autoAbort` {boolean} Whether this stream should automatically
2524+
error if `.destroy()` is called without an error before the stream has emitted `'finish'`.
25112525
* `signal` {AbortSignal} A signal representing possible cancellation.
25122526

25132527
<!-- eslint-disable no-useless-constructor -->
@@ -2865,6 +2879,8 @@ changes:
28652879
[`stream._construct()`][readable-_construct] method.
28662880
* `autoDestroy` {boolean} Whether this stream should automatically call
28672881
`.destroy()` on itself after ending. **Default:** `true`.
2882+
* `autoAbort` {boolean} Whether this stream should automatically
2883+
error if `.destroy()` is called without an error before the stream has emitted `'end'`.
28682884
* `signal` {AbortSignal} A signal representing possible cancellation.
28692885

28702886
<!-- eslint-disable no-useless-constructor -->

lib/internal/streams/destroy.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const {
1414
kDestroyed,
1515
isDestroyed,
1616
isFinished,
17-
isServerRequest
17+
isServerRequest,
18+
isReadableFinished,
19+
isWritableEnded
1820
} = require('internal/streams/utils');
1921

2022
const kDestroy = Symbol('kDestroy');
@@ -86,6 +88,14 @@ function _destroy(self, err, cb) {
8688
const r = self._readableState;
8789
const w = self._writableState;
8890

91+
if (!err) {
92+
if (r?.autoAbort && !isReadableFinished(self)) {
93+
err = new AbortError();
94+
} else if (w?.autoAbort && !isWritableEnded(self)) {
95+
err = new AbortError();
96+
}
97+
}
98+
8999
checkError(err, w, r);
90100

91101
if (w) {

lib/internal/streams/readable.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ function ReadableState(options, stream, isDuplex) {
174174

175175
this.dataEmitted = false;
176176

177+
this.autoAbort = options?.autoAbort ?? false;
178+
177179
this.decoder = null;
178180
this.encoding = null;
179181
if (options && options.encoding) {

lib/internal/streams/writable.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ function WritableState(options, stream, isDuplex) {
196196
// depending on emitClose.
197197
this.closeEmitted = false;
198198

199+
this.autoAbort = options?.autoAbort ?? false;
200+
199201
this[kOnFinished] = [];
200202
}
201203

lib/net.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ function Socket(options) {
324324
// For backwards compat do not emit close on destroy.
325325
options.emitClose = false;
326326
options.autoDestroy = true;
327+
options.autoAbort = false;
327328
// Handle strings directly.
328329
options.decodeStrings = false;
329330
stream.Duplex.call(this, options);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { Readable, Writable } = require('stream');
5+
const assert = require('assert');
6+
7+
{
8+
const w = new Writable({
9+
write() {
10+
11+
}
12+
});
13+
w.on('error', common.mustCall((err) => {
14+
assert.strictEqual(err.name, 'AbortError');
15+
}));
16+
w.destroy();
17+
}
18+
19+
{
20+
const r = new Readable({
21+
read() {
22+
23+
}
24+
});
25+
r.on('error', common.mustCall((err) => {
26+
assert.strictEqual(err.name, 'AbortError');
27+
}));
28+
r.destroy();
29+
}

0 commit comments

Comments
 (0)