Skip to content

Commit 587cd2f

Browse files
authored
Add payloadAsStream option (#308)
Signed-off-by: Matteo Collina <[email protected]>
1 parent 069dfc7 commit 587cd2f

6 files changed

Lines changed: 392 additions & 23 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ Injects a fake request into an HTTP server.
168168
- `signal` - An `AbortSignal` that may be used to abort an ongoing request. Requires Node v16+.
169169
- `Request` - Optional type from which the `request` object should inherit
170170
instead of `stream.Readable`
171+
- `payloadAsStream` - if set to `true`, the response will be streamed and not accumulated; in this case `res.payload`, `res.rawPayload` will be undefined.
171172
- `callback` - the callback function using the signature `function (err, res)` where:
172173
- `err` - error object
173174
- `res` - a response object where:

lib/request.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ function Request (options) {
182182
this._lightMyRequest = {
183183
payload,
184184
isDone: false,
185-
simulate: options.simulate || {}
185+
simulate: options.simulate || {},
186+
payloadAsStream: options.payloadAsStream,
187+
signal: options.signal
186188
}
187189

188190
const signal = options.signal

lib/response.js

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
'use strict'
22

33
const http = require('node:http')
4-
const { Writable, Readable } = require('node:stream')
4+
const { Writable, Readable, addAbortSignal } = require('node:stream')
55
const util = require('node:util')
66

77
const setCookie = require('set-cookie-parser')
88

99
function Response (req, onEnd, reject) {
1010
http.ServerResponse.call(this, req)
1111

12-
this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
12+
if (req._lightMyRequest?.payloadAsStream) {
13+
this._lightMyRequest = { headers: null, trailers: {}, stream: new Readable({ read () {} }) }
14+
const signal = req._lightMyRequest.signal
15+
16+
if (signal) {
17+
addAbortSignal(signal, this._lightMyRequest.stream)
18+
}
19+
} else {
20+
this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
21+
}
1322
// This forces node@8 to always render the headers
1423
this.setHeader('foo', 'bar'); this.removeHeader('foo')
1524

@@ -26,21 +35,37 @@ function Response (req, onEnd, reject) {
2635
}
2736
process.nextTick(() => onEnd(null, payload))
2837
}
38+
this._lightMyRequest.onEndSuccess = onEndSuccess
2939

3040
const onEndFailure = (err) => {
3141
if (called) return
3242
called = true
33-
if (this._promiseCallback) {
34-
return process.nextTick(() => reject(err))
43+
if (this._lightMyRequest.stream) {
44+
const res = generatePayload(this)
45+
res.raw.req = req
46+
this._lightMyRequest.stream._read = function () {
47+
this.destroy(err || new Error('premature close'))
48+
}
49+
onEndSuccess(res)
50+
} else {
51+
if (this._promiseCallback) {
52+
return process.nextTick(() => reject(err))
53+
}
54+
process.nextTick(() => onEnd(err, null))
3555
}
36-
process.nextTick(() => onEnd(err, null))
3756
}
3857

39-
this.once('finish', () => {
40-
const res = generatePayload(this)
41-
res.raw.req = req
42-
onEndSuccess(res)
43-
})
58+
if (this._lightMyRequest.stream) {
59+
this.once('finish', () => {
60+
this._lightMyRequest.stream.push(null)
61+
})
62+
} else {
63+
this.once('finish', () => {
64+
const res = generatePayload(this)
65+
res.raw.req = req
66+
onEndSuccess(res)
67+
})
68+
}
4469

4570
this.connection.once('error', onEndFailure)
4671

@@ -64,6 +89,10 @@ Response.prototype.writeHead = function () {
6489

6590
copyHeaders(this)
6691

92+
if (this._lightMyRequest.stream) {
93+
this._lightMyRequest.onEndSuccess(generatePayload(this))
94+
}
95+
6796
return result
6897
}
6998

@@ -72,7 +101,11 @@ Response.prototype.write = function (data, encoding, callback) {
72101
clearTimeout(this.timeoutHandle)
73102
}
74103
http.ServerResponse.prototype.write.call(this, data, encoding, callback)
75-
this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
104+
if (this._lightMyRequest.stream) {
105+
this._lightMyRequest.stream.push(Buffer.from(data, encoding))
106+
} else {
107+
this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
108+
}
76109
return true
77110
}
78111

@@ -129,22 +162,32 @@ function generatePayload (response) {
129162
}
130163
}
131164

132-
// Prepare payload and trailers
133-
const rawBuffer = Buffer.concat(response._lightMyRequest.payloadChunks)
134-
res.rawPayload = rawBuffer
135-
136-
// we keep both of them for compatibility reasons
137-
res.payload = rawBuffer.toString()
138-
res.body = res.payload
139165
res.trailers = response._lightMyRequest.trailers
140166

141-
// Prepare payload parsers
142-
res.json = function parseJsonPayload () {
143-
return JSON.parse(res.payload)
167+
if (response._lightMyRequest.payloadChunks) {
168+
// Prepare payload and trailers
169+
const rawBuffer = Buffer.concat(response._lightMyRequest.payloadChunks)
170+
res.rawPayload = rawBuffer
171+
172+
// we keep both of them for compatibility reasons
173+
res.payload = rawBuffer.toString()
174+
res.body = res.payload
175+
176+
// Prepare payload parsers
177+
res.json = function parseJsonPayload () {
178+
return JSON.parse(res.payload)
179+
}
180+
} else {
181+
res.json = function () {
182+
throw new Error('Response payload is not available with payloadAsStream: true')
183+
}
144184
}
145185

146186
// Provide stream Readable for advanced user
147187
res.stream = function streamPayload () {
188+
if (response._lightMyRequest.stream) {
189+
return response._lightMyRequest.stream
190+
}
148191
return Readable.from(response._lightMyRequest.payloadChunks)
149192
}
150193

@@ -179,7 +222,7 @@ function copyHeaders (response) {
179222
// Add raw headers
180223
;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
181224
const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
182-
const field = response._header.match(regex)
225+
const field = response._header?.match(regex)
183226
if (field) {
184227
response._lightMyRequest.headers[name.toLowerCase()] = field[1]
185228
}

0 commit comments

Comments
 (0)