You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.3 KiB

3 years ago
'use strict'
const http = require('http')
const { Writable } = require('stream')
const util = require('util')
const setCookie = require('set-cookie-parser')
function Response (req, onEnd, reject) {
http.ServerResponse.call(this, {
method: req.method,
httpVersionMajor: 1,
httpVersionMinor: 1
})
this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
// This forces node@8 to always render the headers
this.setHeader('foo', 'bar'); this.removeHeader('foo')
this.assignSocket(getNullSocket())
this._promiseCallback = typeof reject === 'function'
let called = false
const onEndSuccess = (payload) => {
// no need to early-return if already called because this handler is bound `once`
called = true
if (this._promiseCallback) {
return process.nextTick(() => onEnd(payload))
}
process.nextTick(() => onEnd(null, payload))
}
const onEndFailure = (err) => {
if (called) return
called = true
if (this._promiseCallback) {
return process.nextTick(() => reject(err))
}
process.nextTick(() => onEnd(err, null))
}
this.once('finish', () => {
const res = generatePayload(this)
res.raw.req = req
onEndSuccess(res)
})
this.connection.once('error', onEndFailure)
this.once('error', onEndFailure)
this.once('close', onEndFailure)
}
util.inherits(Response, http.ServerResponse)
Response.prototype.setTimeout = function (msecs, callback) {
this.timeoutHandle = setTimeout(() => {
this.emit('timeout')
}, msecs)
this.on('timeout', callback)
return this
}
Response.prototype.writeHead = function () {
const result = http.ServerResponse.prototype.writeHead.apply(this, arguments)
copyHeaders(this)
return result
}
Response.prototype.write = function (data, encoding, callback) {
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle)
}
http.ServerResponse.prototype.write.call(this, data, encoding, callback)
this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
return true
}
Response.prototype.end = function (data, encoding, callback) {
if (data) {
this.write(data, encoding)
}
http.ServerResponse.prototype.end.call(this, callback)
this.emit('finish')
// We need to emit 'close' otherwise stream.finished() would
// not pick it up on Node v16
this.destroy()
}
Response.prototype.destroy = function (error) {
if (this.destroyed) return
this.destroyed = true
if (error) {
process.nextTick(() => this.emit('error', error))
}
process.nextTick(() => this.emit('close'))
}
Response.prototype.addTrailers = function (trailers) {
for (const key in trailers) {
this._lightMyRequest.trailers[key.toLowerCase().trim()] = trailers[key].toString().trim()
}
}
function generatePayload (response) {
if (response._lightMyRequest.headers === null) {
copyHeaders(response)
}
// Prepare response object
const res = {
raw: {
res: response
},
headers: response._lightMyRequest.headers,
statusCode: response.statusCode,
statusMessage: response.statusMessage,
trailers: {},
get cookies () {
return setCookie.parse(this)
}
}
// Prepare payload and trailers
const rawBuffer = Buffer.concat(response._lightMyRequest.payloadChunks)
res.rawPayload = rawBuffer
// we keep both of them for compatibility reasons
res.payload = rawBuffer.toString()
res.body = res.payload
res.trailers = response._lightMyRequest.trailers
// Prepare payload parsers
res.json = function parseJsonPayload () {
if (res.headers['content-type'].indexOf('application/json') < 0) {
throw new Error('The content-type of the response is not application/json')
}
return JSON.parse(res.payload)
}
return res
}
// Throws away all written data to prevent response from buffering payload
function getNullSocket () {
return new Writable({
write (chunk, encoding, callback) {
setImmediate(callback)
}
})
}
function copyHeaders (response) {
response._lightMyRequest.headers = Object.assign({}, response.getHeaders())
// Add raw headers
;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
const field = response._header.match(regex)
if (field) {
response._lightMyRequest.headers[name.toLowerCase()] = field[1]
}
})
}
module.exports = Response