Skip to content

Commit 27df3b6

Browse files
authored
1 parent 49a3207 commit 27df3b6

File tree

2 files changed

+121
-9
lines changed

2 files changed

+121
-9
lines changed

__tests__/response/body.test.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,111 @@ describe('res.body=', () => {
126126
res.body = body
127127
assert.strictEqual(body.listenerCount('error'), 0)
128128
})
129+
130+
it('should cleanup original stream when replaced by new stream', () => {
131+
const res = response()
132+
const stream1 = new Stream.PassThrough()
133+
const stream2 = new Stream.PassThrough()
134+
135+
res.body = stream1
136+
res.body = stream2
137+
138+
assert.strictEqual(stream1.destroyed, true)
139+
})
140+
141+
it('should cleanup original stream when replaced by null', () => {
142+
const res = response()
143+
const stream = new Stream.PassThrough()
144+
145+
res.body = stream
146+
res.body = null
147+
148+
assert.strictEqual(stream.destroyed, true)
149+
})
150+
151+
it('should not throw unhandled errors when replacing failing stream', async () => {
152+
const res = response()
153+
154+
const stream1 = new Stream.Readable({
155+
read () {
156+
}
157+
})
158+
159+
const stream2 = new Stream.PassThrough()
160+
161+
res.body = stream1
162+
res.body = stream2
163+
164+
await new Promise((resolve) => {
165+
process.nextTick(() => {
166+
stream1.emit('error', new Error('stream1 error'))
167+
setTimeout(resolve, 10)
168+
})
169+
})
170+
})
171+
172+
it('should handle multiple sequential stream replacements', () => {
173+
const res = response()
174+
const stream1 = new Stream.PassThrough()
175+
const stream2 = new Stream.PassThrough()
176+
const stream3 = new Stream.PassThrough()
177+
178+
res.body = stream1
179+
res.body = stream2
180+
res.body = stream3
181+
182+
assert.strictEqual(stream1.destroyed, true)
183+
assert.strictEqual(stream2.destroyed, true)
184+
assert.strictEqual(stream3.destroyed, false)
185+
})
186+
187+
it('should handle four sequential stream replacements', () => {
188+
const res = response()
189+
const stream1 = new Stream.PassThrough()
190+
const stream2 = new Stream.PassThrough()
191+
const stream3 = new Stream.PassThrough()
192+
const stream4 = new Stream.PassThrough()
193+
194+
res.body = stream1
195+
res.body = stream2
196+
res.body = stream3
197+
res.body = stream4
198+
199+
assert.strictEqual(stream1.destroyed, true)
200+
assert.strictEqual(stream2.destroyed, true)
201+
assert.strictEqual(stream3.destroyed, true)
202+
assert.strictEqual(stream4.destroyed, false)
203+
})
204+
205+
it('should cleanup stream when replaced by string', () => {
206+
const res = response()
207+
const stream = new Stream.PassThrough()
208+
209+
res.body = stream
210+
res.body = 'hello'
211+
212+
assert.strictEqual(stream.destroyed, true)
213+
})
214+
215+
it('should cleanup stream when replaced by buffer', () => {
216+
const res = response()
217+
const stream = new Stream.PassThrough()
218+
219+
res.body = stream
220+
res.body = Buffer.from('hello')
221+
222+
assert.strictEqual(stream.destroyed, true)
223+
})
224+
225+
it('should cleanup stream when replaced by object', () => {
226+
const res = response()
227+
const stream = new Stream.PassThrough()
228+
229+
res.body = stream
230+
res.body = { foo: 'bar' }
231+
232+
assert.strictEqual(stream.destroyed, true)
233+
})
129234
})
130235

131236
describe('when a buffer is given', () => {

lib/response.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ module.exports = {
135135
set body (val) {
136136
const original = this._body
137137
this._body = val
138+
139+
const cleanupPreviousStream = () => {
140+
if (original && isStream(original)) {
141+
original.once('error', () => {})
142+
destroy(original)
143+
}
144+
}
145+
138146
// no content
139147

140148
if (val == null) {
@@ -149,13 +157,7 @@ module.exports = {
149157
this.remove('Content-Type')
150158
this.remove('Content-Length')
151159
this.remove('Transfer-Encoding')
152-
153-
const shouldDestroyOriginal = original && isStream(original)
154-
if (shouldDestroyOriginal) {
155-
// Ignore errors during cleanup to prevent unhandled exceptions when destroying the stream
156-
original.once('error', () => {})
157-
destroy(original)
158-
}
160+
cleanupPreviousStream()
159161
return
160162
}
161163

@@ -169,22 +171,24 @@ module.exports = {
169171
if (typeof val === 'string') {
170172
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
171173
this.length = Buffer.byteLength(val)
174+
cleanupPreviousStream()
172175
return
173176
}
174177

175178
// buffer
176179
if (Buffer.isBuffer(val)) {
177180
if (setType) this.type = 'bin'
178181
this.length = val.length
182+
cleanupPreviousStream()
179183
return
180184
}
181185

182186
// stream
183187
if (isStream(val)) {
184188
onFinish(this.res, destroy.bind(null, val))
185189
if (original !== val) {
186-
// overwriting
187190
if (original != null) this.remove('Content-Length')
191+
cleanupPreviousStream()
188192
}
189193

190194
if (setType) this.type = 'bin'
@@ -194,13 +198,15 @@ module.exports = {
194198
// ReadableStream
195199
if (val instanceof ReadableStream) {
196200
if (setType) this.type = 'bin'
201+
cleanupPreviousStream()
197202
return
198203
}
199204

200205
// blob
201206
if (val instanceof Blob) {
202207
if (setType) this.type = 'bin'
203208
this.length = val.size
209+
cleanupPreviousStream()
204210
return
205211
}
206212

@@ -212,13 +218,14 @@ module.exports = {
212218
for (const key of headers.keys()) {
213219
this.set(key, headers.get(key))
214220
}
215-
221+
cleanupPreviousStream()
216222
return
217223
}
218224

219225
// json
220226
this.remove('Content-Length')
221227
if (!this.type || !/\bjson\b/i.test(this.type)) this.type = 'json'
228+
cleanupPreviousStream()
222229
},
223230

224231
/**

0 commit comments

Comments
 (0)