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
			
		
		
			
		
	
	
					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 |