From 220a391584098628c757ebba6ba138121e373eb0 Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 01:38:42 +0300
Subject: [PATCH 1/9] class Chain

---
 index.js        | 144 +-----------------------------------------------
 lib/Chain.js    | 105 +++++++++++++++++++++++++++++++++++
 lib/doInject.js |  66 ++++++++++++++++++++++
 lib/response.js |  10 +---
 4 files changed, 174 insertions(+), 151 deletions(-)
 create mode 100644 lib/Chain.js
 create mode 100644 lib/doInject.js

diff --git a/index.js b/index.js
index e18da03..aa5fe43 100644
--- a/index.js
+++ b/index.js
@@ -1,12 +1,9 @@
 'use strict'
 
-const assert = require('assert')
 const Request = require('./lib/request')
 const Response = require('./lib/response')
-
-const errorMessage = 'The dispatch function has already been invoked'
-
-const optsValidator = require('./lib/configValidator')
+const Chain = require('./lib/Chain')
+const doInject = require('./lib/doInject')
 
 function inject (dispatchFunc, options, callback) {
   if (typeof callback === 'undefined') {
@@ -16,143 +13,6 @@ function inject (dispatchFunc, options, callback) {
   }
 }
 
-function makeRequest (dispatchFunc, server, req, res) {
-  req.once('error', function (err) {
-    if (this.destroyed) res.destroy(err)
-  })
-
-  req.once('close', function () {
-    if (this.destroyed && !this._error) res.destroy()
-  })
-
-  return req.prepare(() => dispatchFunc.call(server, req, res))
-}
-
-function doInject (dispatchFunc, options, callback) {
-  options = (typeof options === 'string' ? { url: options } : options)
-
-  if (options.validate !== false) {
-    assert(typeof dispatchFunc === 'function', 'dispatchFunc should be a function')
-    const isOptionValid = optsValidator(options)
-    if (!isOptionValid) {
-      throw new Error(optsValidator.errors.map(e => e.message))
-    }
-  }
-
-  const server = options.server || {}
-
-  const RequestConstructor = options.Request
-    ? Request.CustomRequest
-    : Request
-
-  // Express.js detection
-  if (dispatchFunc.request && dispatchFunc.request.app === dispatchFunc) {
-    Object.setPrototypeOf(Object.getPrototypeOf(dispatchFunc.request), RequestConstructor.prototype)
-    Object.setPrototypeOf(Object.getPrototypeOf(dispatchFunc.response), Response.prototype)
-  }
-
-  if (typeof callback === 'function') {
-    const req = new RequestConstructor(options)
-    const res = new Response(req, callback)
-
-    return makeRequest(dispatchFunc, server, req, res)
-  } else {
-    return new Promise((resolve, reject) => {
-      const req = new RequestConstructor(options)
-      const res = new Response(req, resolve, reject)
-
-      makeRequest(dispatchFunc, server, req, res)
-    })
-  }
-}
-
-function Chain (dispatch, option) {
-  if (typeof option === 'string') {
-    this.option = { url: option }
-  } else {
-    this.option = Object.assign({}, option)
-  }
-
-  this.dispatch = dispatch
-  this._hasInvoked = false
-  this._promise = null
-
-  if (this.option.autoStart !== false) {
-    process.nextTick(() => {
-      if (!this._hasInvoked) {
-        this.end()
-      }
-    })
-  }
-}
-
-const httpMethods = [
-  'delete',
-  'get',
-  'head',
-  'options',
-  'patch',
-  'post',
-  'put',
-  'trace'
-]
-
-httpMethods.forEach(method => {
-  Chain.prototype[method] = function (url) {
-    if (this._hasInvoked === true || this._promise) {
-      throw new Error(errorMessage)
-    }
-    this.option.url = url
-    this.option.method = method.toUpperCase()
-    return this
-  }
-})
-
-const chainMethods = [
-  'body',
-  'cookies',
-  'headers',
-  'payload',
-  'query'
-]
-
-chainMethods.forEach(method => {
-  Chain.prototype[method] = function (value) {
-    if (this._hasInvoked === true || this._promise) {
-      throw new Error(errorMessage)
-    }
-    this.option[method] = value
-    return this
-  }
-})
-
-Chain.prototype.end = function (callback) {
-  if (this._hasInvoked === true || this._promise) {
-    throw new Error(errorMessage)
-  }
-  this._hasInvoked = true
-  if (typeof callback === 'function') {
-    doInject(this.dispatch, this.option, callback)
-  } else {
-    this._promise = doInject(this.dispatch, this.option)
-    return this._promise
-  }
-}
-
-Object.getOwnPropertyNames(Promise.prototype).forEach(method => {
-  if (method === 'constructor') return
-  Chain.prototype[method] = function (...args) {
-    if (!this._promise) {
-      if (this._hasInvoked === true) {
-        throw new Error(errorMessage)
-      }
-      this._hasInvoked = true
-      this._promise = doInject(this.dispatch, this.option)
-    }
-    return this._promise[method](...args)
-  }
-})
-
 function isInjection (obj) {
   return (
     obj instanceof Request ||
diff --git a/lib/Chain.js b/lib/Chain.js
new file mode 100644
index 0000000..68aa4b2
--- /dev/null
+++ b/lib/Chain.js
@@ -0,0 +1,105 @@
+const doInject = require('./doInject')
+
+const errorMessage = 'The dispatch function has already been invoked'
+
+class Chain {
+  _hasInvoked = false
+  _promise = null
+  option
+  dispatch
+
+  constructor (dispatch, option) {
+    this.dispatch = dispatch
+    if (typeof option === 'string') {
+      this.option = { url: option }
+    } else {
+      this.option = Object.assign({}, option)
+    }
+
+    if (this.option.autoStart !== false) {
+      process.nextTick(() => {
+        if (!this._hasInvoked) {
+          this.end()
+        }
+      })
+    }
+  }
+
+  /**
+   * @private
+   * @param {string} method
+   * @param {string} url
+   */
+  wrapHttpMethod (method, url) {
+    if (this._hasInvoked === true || this._promise) {
+      throw new Error(errorMessage)
+    }
+    this.option.url = url
+    this.option.method = method.toUpperCase()
+    return this
+  }
+
+  delete (url) { return this.wrapHttpMethod('delete', url) }
+  get (url) { return this.wrapHttpMethod('get', url) }
+  head (url) { return this.wrapHttpMethod('head', url) }
+  options (url) { return this.wrapHttpMethod('options', url) }
+  patch (url) { return this.wrapHttpMethod('patch', url) }
+  post (url) { return this.wrapHttpMethod('post', url) }
+  put (url) { return this.wrapHttpMethod('put', url) }
+  trace (url) { return this.wrapHttpMethod('trace', url) }
+
+  /**
+   * @private
+   * @param {string} method
+   * @param {string} url
+   */
+  wrapChainMethod (method, value) {
+    if (this._hasInvoked === true || this._promise) {
+      throw new Error(errorMessage)
+    }
+    this.option[method] = value
+    return this
+  }
+
+  body (url) { return this.wrapChainMethod('body', url) }
+  cookies (url) { return this.wrapChainMethod('cookies', url) }
+  headers (url) { return this.wrapChainMethod('headers', url) }
+  payload (url) { return this.wrapChainMethod('payload', url) }
+  query (url) { return this.wrapChainMethod('query', url) }
+
+  end (callback) {
+    if (this._hasInvoked === true || this._promise) {
+      throw new Error(errorMessage)
+    }
+    this._hasInvoked = true
+    if (typeof callback === 'function') {
+      doInject(this.dispatch, this.option, callback)
+    } else {
+      this._promise = doInject(this.dispatch, this.option)
+      return this._promise
+    }
+  }
+
+  /**
+   * @private
+   * @template {keyof Promise} T
+   * @param {T} method
+   * @param  {Parameters<Promise[T]>} args
+   */
+  promisify (method, args) {
+    if (!this._promise) {
+      if (this._hasInvoked === true) {
+        throw new Error(errorMessage)
+      }
+      this._hasInvoked = true
+      this._promise = doInject(this.dispatch, this.option)
+    }
+    return this._promise[method](...args)
+  }
+
+  then (...args) { return this.promisify('then', args) }
+  catch (...args) { return this.promisify('catch', args) }
+  finally (...args) { return this.promisify('finally', args) }
+}
+
+module.exports = Chain
diff --git a/lib/doInject.js b/lib/doInject.js
new file mode 100644
index 0000000..fe3a8cb
--- /dev/null
+++ b/lib/doInject.js
@@ -0,0 +1,66 @@
+const assert = require('assert')
+const optsValidator = require('./configValidator')
+const Request = require('./request')
+const Response = require('./response')
+
+function promisify (fn) {
+  if (fn) {
+    return { ret: Promise.resolve(), cb: fn }
+  }
+  let resolve, reject
+  const ret = new Promise((_resolve, _reject) => {
+    resolve = _resolve
+    reject = _reject
+  })
+  return {
+    ret,
+    cb: (err, res) => {
+      err ? reject(err) : resolve(res)
+    }
+  }
+}
+
+function makeRequest (dispatchFunc, server, req, res) {
+  req.once('error', function (err) {
+    if (this.destroyed) res.destroy(err)
+  })
+
+  req.once('close', function () {
+    if (this.destroyed && !this._error) res.destroy()
+  })
+
+  return req.prepare(() => dispatchFunc.call(server, req, res))
+}
+
+function doInject (dispatchFunc, options, callback) {
+  options = (typeof options === 'string' ? { url: options } : options)
+
+  if (options.validate !== false) {
+    assert(typeof dispatchFunc === 'function', 'dispatchFunc should be a function')
+    const isOptionValid = optsValidator(options)
+    if (!isOptionValid) {
+      throw new Error(optsValidator.errors.map(e => e.message))
+    }
+  }
+
+  const server = options.server || {}
+
+  const RequestConstructor = options.Request
+    ? Request.CustomRequest
+    : Request
+
+  // Express.js detection
+  if (dispatchFunc.request && dispatchFunc.request.app === dispatchFunc) {
+    Object.setPrototypeOf(Object.getPrototypeOf(dispatchFunc.request), RequestConstructor.prototype)
+    Object.setPrototypeOf(Object.getPrototypeOf(dispatchFunc.response), Response.prototype)
+  }
+
+  const { ret, cb } = promisify(callback)
+
+  const req = new RequestConstructor(options)
+  const res = new Response(req, cb)
+
+  return Promise.resolve().then(() => makeRequest(dispatchFunc, server, req, res)).then(() => ret)
+}
+
+module.exports = doInject
diff --git a/lib/response.js b/lib/response.js
index bcdf60b..b67de74 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -6,7 +6,7 @@ const util = require('util')
 
 const setCookie = require('set-cookie-parser')
 
-function Response (req, onEnd, reject) {
+function Response (req, onEnd) {
   http.ServerResponse.call(this, req)
 
   this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
@@ -15,24 +15,16 @@ function Response (req, onEnd, reject) {
 
   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))
   }
 

From 2d30a99ee7d9a4b5487ae94a9a6bd17eac4c52ed Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 01:57:33 +0300
Subject: [PATCH 2/9] class response

---
 lib/response.js    | 268 +++++++++++++++++++++++----------------------
 test/index.test.js |  13 +++
 2 files changed, 150 insertions(+), 131 deletions(-)

diff --git a/lib/response.js b/lib/response.js
index b67de74..9db10f1 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -2,175 +2,181 @@
 
 const http = require('http')
 const { Writable } = require('stream')
-const util = require('util')
 
 const setCookie = require('set-cookie-parser')
 
-function Response (req, onEnd) {
-  http.ServerResponse.call(this, req)
-
-  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())
-
-  let called = false
-  const onEndSuccess = (payload) => {
-    // no need to early-return if already called because this handler is bound `once`
-    called = true
-    process.nextTick(() => onEnd(null, payload))
-  }
-
-  const onEndFailure = (err) => {
-    if (called) return
-    called = true
-    process.nextTick(() => onEnd(err, null))
-  }
-
-  this.once('finish', () => {
-    const res = generatePayload(this)
-    res.raw.req = req
-    onEndSuccess(res)
+// Throws away all written data to prevent response from buffering payload
+function getNullSocket () {
+  return new Writable({
+    write (chunk, encoding, callback) {
+      setImmediate(callback)
+    }
   })
+}
+
+class Response extends http.ServerResponse {
+  constructor (req, onEnd) {
+    super(req)
+    this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
+    this.setHeader('foo', 'bar'); this.removeHeader('foo')
+    this.assignSocket(getNullSocket())
+    let called = false
+    const onEndSuccess = (payload) => {
+      // no need to early-return if already called because this handler is bound `once`
+      called = true
+      process.nextTick(() => onEnd(null, payload))
+    }
 
-  this.connection.once('error', onEndFailure)
+    const onEndFailure = (err) => {
+      if (called) return
+      called = true
+      process.nextTick(() => onEnd(err, null))
+    }
 
-  this.once('error', onEndFailure)
+    this.once('finish', () => {
+      const res = this.generatePayload(req)
+      onEndSuccess(res)
+    })
 
-  this.once('close', onEndFailure)
-}
+    this.socket.once('error', onEndFailure)
 
-util.inherits(Response, http.ServerResponse)
+    this.once('error', onEndFailure)
 
-Response.prototype.setTimeout = function (msecs, callback) {
-  this.timeoutHandle = setTimeout(() => {
-    this.emit('timeout')
-  }, msecs)
-  this.on('timeout', callback)
-  return this
-}
+    this.once('close', onEndFailure)
+  }
 
-Response.prototype.writeHead = function () {
-  const result = http.ServerResponse.prototype.writeHead.apply(this, arguments)
+  setTimeout (msecs, callback) {
+    this.timeoutHandle = setTimeout(() => {
+      this.emit('timeout')
+    }, msecs)
+    this.on('timeout', callback)
+    return this
+  }
 
-  copyHeaders(this)
+  writeHead (...opt) {
+    const result = super.writeHead(...opt)
 
-  return result
-}
+    this.copyHeaders()
 
-Response.prototype.write = function (data, encoding, callback) {
-  if (this.timeoutHandle) {
-    clearTimeout(this.timeoutHandle)
+    return result
   }
-  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)
+  write (data, encoding, callback) {
+    if (this.timeoutHandle) {
+      clearTimeout(this.timeoutHandle)
+    }
+    super.write(data, encoding, callback)
+    this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
+    return true
   }
 
-  http.ServerResponse.prototype.end.call(this, callback)
-
-  this.emit('finish')
+  end (data, encoding, callback) {
+    if (data) {
+      this.write(data, encoding)
+    }
 
-  // We need to emit 'close' otherwise stream.finished() would
-  // not pick it up on Node v16
+    super.end(callback)
 
-  this.destroy()
-}
+    this.emit('finish')
 
-Response.prototype.destroy = function (error) {
-  if (this.destroyed) return
-  this.destroyed = true
+    // We need to emit 'close' otherwise stream.finished() would
+    // not pick it up on Node v16
 
-  if (error) {
-    process.nextTick(() => this.emit('error', error))
+    this.destroy()
   }
 
-  process.nextTick(() => this.emit('close'))
-}
+  destroy (error) {
+    if (this.destroyed) return
+    this.destroyed = true
 
-Response.prototype.addTrailers = function (trailers) {
-  for (const key in trailers) {
-    this._lightMyRequest.trailers[key.toLowerCase().trim()] = trailers[key].toString().trim()
-  }
-}
+    if (error) {
+      process.nextTick(() => this.emit('error', error))
+    }
 
-function generatePayload (response) {
-  // This seems only to happen when using `fastify-express` - see https://github.com/fastify/fastify-express/issues/47
-  /* istanbul ignore if */
-  if (response._lightMyRequest.headers === null) {
-    copyHeaders(response)
+    process.nextTick(() => this.emit('close'))
   }
-  serializeHeaders(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)
+
+  addTrailers (trailers) {
+    for (const key in trailers) {
+      this._lightMyRequest.trailers[key.toLowerCase().trim()] = trailers[key].toString().trim()
     }
   }
 
-  // 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
+  /**
+   * @private
+   * @param {Request} req
+   * @returns
+   */
+  generatePayload (req) {
+    // This seems only to happen when using `fastify-express` - see https://github.com/fastify/fastify-express/issues/47
+    /* istanbul ignore if */
+    if (this._lightMyRequest.headers === null) {
+      this.copyHeaders()
+    }
+    this.serializeHeaders()
+    // Prepare response object
+    const res = {
+      raw: {
+        res: this,
+        req
+      },
+      headers: this._lightMyRequest.headers,
+      statusCode: this.statusCode,
+      statusMessage: this.statusMessage,
+      trailers: {},
+      get cookies () {
+        return setCookie.parse(this)
+      }
+    }
 
-  // Prepare payload parsers
-  res.json = function parseJsonPayload () {
-    return JSON.parse(res.payload)
-  }
+    // Prepare payload and trailers
+    const rawBuffer = Buffer.concat(this._lightMyRequest.payloadChunks)
+    res.rawPayload = rawBuffer
 
-  return res
-}
+    // we keep both of them for compatibility reasons
+    res.payload = rawBuffer.toString()
+    res.body = res.payload
+    res.trailers = this._lightMyRequest.trailers
 
-// Throws away all written data to prevent response from buffering payload
-function getNullSocket () {
-  return new Writable({
-    write (chunk, encoding, callback) {
-      setImmediate(callback)
+    // Prepare payload parsers
+    res.json = function parseJsonPayload () {
+      return JSON.parse(res.payload)
     }
-  })
-}
 
-function serializeHeaders (response) {
-  const headers = response._lightMyRequest.headers
+    return res
+  }
 
-  for (const headerName of Object.keys(headers)) {
-    const headerValue = headers[headerName]
-    if (Array.isArray(headerValue)) {
-      headers[headerName] = headerValue.map(value => '' + value)
-    } else {
-      headers[headerName] = '' + headerValue
+  /**
+   * @private
+   */
+  serializeHeaders () {
+    const headers = this._lightMyRequest.headers
+
+    for (const headerName of Object.keys(headers)) {
+      const headerValue = headers[headerName]
+      if (Array.isArray(headerValue)) {
+        headers[headerName] = headerValue.map(value => '' + value)
+      } else {
+        headers[headerName] = '' + headerValue
+      }
     }
   }
-}
-
-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]
-    }
-  })
+  /**
+   * @private
+   */
+  copyHeaders () {
+    this._lightMyRequest.headers = Object.assign({}, this.getHeaders())
+
+    // Add raw headers
+    ;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
+      const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
+      const field = this._header.match(regex)
+      if (field) {
+        this._lightMyRequest.headers[name.toLowerCase()] = field[1]
+      }
+    })
+  }
 }
 
 module.exports = Response
diff --git a/test/index.test.js b/test/index.test.js
index ecb1ac7..43d3583 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -1127,6 +1127,19 @@ test('chainable api: backwards compatibility for promise (catch)', (t) => {
     .catch(err => t.ok(err))
 })
 
+test('chainable api: backwards compatibility for promise (finally)', (t) => {
+  t.plan(1)
+
+  function dispatch (req, res) {
+    res.writeHead(200, { 'Content-Type': 'text/plain' })
+    res.end('hello')
+  }
+
+  inject(dispatch)
+    .get('/')
+    .finally(() => t.pass())
+})
+
 test('chainable api: multiple call of then should return the same promise', (t) => {
   t.plan(2)
   let id = 0

From 909950c09f9bfc5d939c01625e9d300ce565ae35 Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 03:04:47 +0300
Subject: [PATCH 3/9] class request

---
 lib/doInject.js |   2 +-
 lib/request.js  | 331 +++++++++++++++++++++++-------------------------
 2 files changed, 160 insertions(+), 173 deletions(-)

diff --git a/lib/doInject.js b/lib/doInject.js
index fe3a8cb..925f8c3 100644
--- a/lib/doInject.js
+++ b/lib/doInject.js
@@ -46,7 +46,7 @@ function doInject (dispatchFunc, options, callback) {
   const server = options.server || {}
 
   const RequestConstructor = options.Request
-    ? Request.CustomRequest
+    ? Request.getCustomRequest(options.Request)
     : Request
 
   // Express.js detection
diff --git a/lib/request.js b/lib/request.js
index d5b8782..0ea7b65 100644
--- a/lib/request.js
+++ b/lib/request.js
@@ -3,8 +3,8 @@
 /* eslint no-prototype-builtins: 0 */
 
 const { Readable, addAbortSignal } = require('stream')
-const util = require('util')
 const cookie = require('cookie')
+const util = require('util')
 const assert = require('assert')
 const warning = require('process-warning')()
 
@@ -39,221 +39,208 @@ class MockSocket extends EventEmitter {
   }
 }
 
-/**
- * CustomRequest
- *
- * @constructor
- * @param {Object} options
- * @param {(Object|String)} options.url || options.path
- * @param {String} [options.method='GET']
- * @param {String} [options.remoteAddress]
- * @param {Object} [options.cookies]
- * @param {Object} [options.headers]
- * @param {Object} [options.query]
- * @param {Object} [options.Request]
- * @param {any} [options.payload]
- */
-function CustomRequest (options) {
-  return new _CustomLMRRequest(this)
+class Request extends Readable {
+  /**
+   * Request
+   *
+   * @param {Object} options
+   * @param {(Object|String)} options.url || options.path
+   * @param {String} [options.method='GET']
+   * @param {String} [options.remoteAddress]
+   * @param {Object} [options.cookies]
+   * @param {Object} [options.headers]
+   * @param {Object} [options.query]
+   * @param {any} [options.payload]
+   */
+  constructor (options) {
+    super({ autoDestroy: false })
+    const parsedURL = parseURL(options.url || options.path, options.query)
+
+    this.url = parsedURL.pathname + parsedURL.search
+
+    this.aborted = false
+    this.httpVersionMajor = 1
+    this.httpVersionMinor = 1
+    this.httpVersion = '1.1'
+    this.method = options.method ? options.method.toUpperCase() : 'GET'
+
+    this.headers = {}
+    this.rawHeaders = []
+    const headers = options.headers || {}
+
+    for (const field in headers) {
+      const fieldLowerCase = field.toLowerCase()
+      if (
+        (
+          fieldLowerCase === 'user-agent' ||
+        fieldLowerCase === 'content-type'
+        ) && headers[field] === undefined
+      ) {
+        this.headers[fieldLowerCase] = undefined
+        continue
+      }
+      const value = headers[field]
+      assert(value !== undefined, 'invalid value "undefined" for header ' + field)
+      this.headers[fieldLowerCase] = '' + value
+    }
 
-  function _CustomLMRRequest (obj) {
-    Request.call(obj, {
-      ...options,
-      Request: undefined
-    })
-    Object.assign(this, obj)
+    if (('user-agent' in this.headers) === false) {
+      this.headers['user-agent'] = 'lightMyRequest'
+    }
+    this.headers.host = this.headers.host || options.authority || hostHeaderFromURL(parsedURL)
 
-    for (const fn of Object.keys(Request.prototype)) {
-      this.constructor.prototype[fn] = Request.prototype[fn]
+    if (options.cookies) {
+      const { cookies } = options
+      const cookieValues = Object.keys(cookies).map(key => cookie.serialize(key, cookies[key]))
+      if (this.headers.cookie) {
+        cookieValues.unshift(this.headers.cookie)
+      }
+      this.headers.cookie = cookieValues.join('; ')
     }
 
-    util.inherits(this.constructor, options.Request)
-    return this
-  }
-}
+    this.socket = new MockSocket(options.remoteAddress || '127.0.0.1')
 
-/**
- * Request
- *
- * @constructor
- * @param {Object} options
- * @param {(Object|String)} options.url || options.path
- * @param {String} [options.method='GET']
- * @param {String} [options.remoteAddress]
- * @param {Object} [options.cookies]
- * @param {Object} [options.headers]
- * @param {Object} [options.query]
- * @param {any} [options.payload]
- */
-function Request (options) {
-  Readable.call(this, {
-    autoDestroy: false
-  })
+    Object.defineProperty(this, 'connection', {
+      get () {
+        warning.emit('FST_LIGHTMYREQUEST_DEP01')
+        return this.socket
+      },
+      configurable: true
+    })
 
-  const parsedURL = parseURL(options.url || options.path, options.query)
+    // we keep both payload and body for compatibility reasons
+    let payload = options.payload || options.body || null
+    const payloadResume = payload && typeof payload.resume === 'function'
 
-  this.url = parsedURL.pathname + parsedURL.search
+    if (payload && typeof payload !== 'string' && !payloadResume && !Buffer.isBuffer(payload)) {
+      payload = JSON.stringify(payload)
 
-  this.aborted = false
-  this.httpVersionMajor = 1
-  this.httpVersionMinor = 1
-  this.httpVersion = '1.1'
-  this.method = options.method ? options.method.toUpperCase() : 'GET'
+      if (('content-type' in this.headers) === false) {
+        this.headers['content-type'] = 'application/json'
+      }
+    }
 
-  this.headers = {}
-  this.rawHeaders = []
-  const headers = options.headers || {}
+    // Set the content-length for the corresponding payload if none set
+    if (payload && !payloadResume && !Object.prototype.hasOwnProperty.call(this.headers, 'content-length')) {
+      this.headers['content-length'] = (Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(payload)).toString()
+    }
 
-  for (const field in headers) {
-    const fieldLowerCase = field.toLowerCase()
-    if (
-      (
-        fieldLowerCase === 'user-agent' ||
-        fieldLowerCase === 'content-type'
-      ) && headers[field] === undefined
-    ) {
-      this.headers[fieldLowerCase] = undefined
-      continue
+    for (const header of Object.keys(this.headers)) {
+      this.rawHeaders.push(header, this.headers[header])
     }
-    const value = headers[field]
-    assert(value !== undefined, 'invalid value "undefined" for header ' + field)
-    this.headers[fieldLowerCase] = '' + value
-  }
 
-  if (('user-agent' in this.headers) === false) {
-    this.headers['user-agent'] = 'lightMyRequest'
-  }
-  this.headers.host = this.headers.host || options.authority || hostHeaderFromURL(parsedURL)
+    // Use _lightMyRequest namespace to avoid collision with Node
+    this._lightMyRequest = {
+      payload,
+      isDone: false,
+      simulate: options.simulate || {}
+    }
 
-  if (options.cookies) {
-    const { cookies } = options
-    const cookieValues = Object.keys(cookies).map(key => cookie.serialize(key, cookies[key]))
-    if (this.headers.cookie) {
-      cookieValues.unshift(this.headers.cookie)
+    const signal = options.signal
+    /* istanbul ignore if  */
+    if (signal) {
+      addAbortSignal(signal, this)
     }
-    this.headers.cookie = cookieValues.join('; ')
   }
 
-  this.socket = new MockSocket(options.remoteAddress || '127.0.0.1')
-
-  Object.defineProperty(this, 'connection', {
-    get () {
-      warning.emit('FST_LIGHTMYREQUEST_DEP01')
-      return this.socket
-    },
-    configurable: true
-  })
-
-  // we keep both payload and body for compatibility reasons
-  let payload = options.payload || options.body || null
-  const payloadResume = payload && typeof payload.resume === 'function'
-
-  if (payload && typeof payload !== 'string' && !payloadResume && !Buffer.isBuffer(payload)) {
-    payload = JSON.stringify(payload)
-
-    if (('content-type' in this.headers) === false) {
-      this.headers['content-type'] = 'application/json'
+  prepare (next) {
+    const payload = this._lightMyRequest.payload
+    if (!payload || typeof payload.resume !== 'function') { // does not quack like a stream
+      return next()
     }
-  }
 
-  // Set the content-length for the corresponding payload if none set
-  if (payload && !payloadResume && !Object.prototype.hasOwnProperty.call(this.headers, 'content-length')) {
-    this.headers['content-length'] = (Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(payload)).toString()
-  }
+    const chunks = []
 
-  for (const header of Object.keys(this.headers)) {
-    this.rawHeaders.push(header, this.headers[header])
-  }
+    payload.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
 
-  // Use _lightMyRequest namespace to avoid collision with Node
-  this._lightMyRequest = {
-    payload,
-    isDone: false,
-    simulate: options.simulate || {}
-  }
+    payload.on('end', () => {
+      const payload = Buffer.concat(chunks)
+      this.headers['content-length'] = this.headers['content-length'] || ('' + payload.length)
+      this._lightMyRequest.payload = payload
+      return next()
+    })
 
-  const signal = options.signal
-  /* istanbul ignore if  */
-  if (signal) {
-    addAbortSignal(signal, this)
+    // Force to resume the stream. Needed for Stream 1
+    payload.resume()
   }
 
-  return this
-}
-
-util.inherits(Request, Readable)
-util.inherits(CustomRequest, Request)
+  _read (size) {
+    setImmediate(() => {
+      if (this._lightMyRequest.isDone) {
+        // 'end' defaults to true
+        if (this._lightMyRequest.simulate.end !== false) {
+          this.push(null)
+        }
 
-Request.prototype.prepare = function (next) {
-  const payload = this._lightMyRequest.payload
-  if (!payload || typeof payload.resume !== 'function') { // does not quack like a stream
-    return next()
-  }
+        return
+      }
 
-  const chunks = []
+      this._lightMyRequest.isDone = true
 
-  payload.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
+      if (this._lightMyRequest.payload) {
+        if (this._lightMyRequest.simulate.split) {
+          this.push(this._lightMyRequest.payload.slice(0, 1))
+          this.push(this._lightMyRequest.payload.slice(1))
+        } else {
+          this.push(this._lightMyRequest.payload)
+        }
+      }
 
-  payload.on('end', () => {
-    const payload = Buffer.concat(chunks)
-    this.headers['content-length'] = this.headers['content-length'] || ('' + payload.length)
-    this._lightMyRequest.payload = payload
-    return next()
-  })
+      if (this._lightMyRequest.simulate.error) {
+        this.emit('error', new Error('Simulated'))
+      }
 
-  // Force to resume the stream. Needed for Stream 1
-  payload.resume()
-}
+      if (this._lightMyRequest.simulate.close) {
+        this.emit('close')
+      }
 
-Request.prototype._read = function (size) {
-  setImmediate(() => {
-    if (this._lightMyRequest.isDone) {
       // 'end' defaults to true
       if (this._lightMyRequest.simulate.end !== false) {
         this.push(null)
       }
+    })
+  }
 
-      return
-    }
-
-    this._lightMyRequest.isDone = true
+  destroy (error) {
+    if (this.destroyed || this._lightMyRequest.isDone) return
+    this.destroyed = true
 
-    if (this._lightMyRequest.payload) {
-      if (this._lightMyRequest.simulate.split) {
-        this.push(this._lightMyRequest.payload.slice(0, 1))
-        this.push(this._lightMyRequest.payload.slice(1))
-      } else {
-        this.push(this._lightMyRequest.payload)
-      }
+    if (error) {
+      this._error = true
+      process.nextTick(() => this.emit('error', error))
     }
 
-    if (this._lightMyRequest.simulate.error) {
-      this.emit('error', new Error('Simulated'))
-    }
+    process.nextTick(() => this.emit('close'))
+  }
+}
 
-    if (this._lightMyRequest.simulate.close) {
-      this.emit('close')
-    }
+function Classes (bases) {
+  class _CustomLMRRequest {
+    constructor (...opt) {
+      bases.forEach(Base => {
+        Object.assign(this, new Base(...opt))
+      })
 
-    // 'end' defaults to true
-    if (this._lightMyRequest.simulate.end !== false) {
-      this.push(null)
+      util.inherits(this.constructor, bases[bases.length - 1])
     }
+  }
+  bases.forEach(base => {
+    Object.getOwnPropertyNames(base.prototype)
+      .filter(prop => prop !== 'constructor')
+      .forEach(prop => { _CustomLMRRequest.prototype[prop] = base.prototype[prop] })
   })
+  return _CustomLMRRequest
 }
 
-Request.prototype.destroy = function (error) {
-  if (this.destroyed || this._lightMyRequest.isDone) return
-  this.destroyed = true
-
-  if (error) {
-    this._error = true
-    process.nextTick(() => this.emit('error', error))
-  }
-
-  process.nextTick(() => this.emit('close'))
+/**
+ * @template T
+ * @param {new (opt: import('../types').InjectOptions) => T} CustomRequest
+ * @returns {new (opt: import('../types').InjectOptions) => T & Request}
+ */
+function getCustomRequest (CustomRequest) {
+  return Classes([Request, CustomRequest])
 }
 
 module.exports = Request
 module.exports.Request = Request
-module.exports.CustomRequest = CustomRequest
+module.exports.getCustomRequest = getCustomRequest

From 0fb0a1fd600b0e26cee4bb9746add8b2dfb7a72d Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 04:33:17 +0300
Subject: [PATCH 4/9] use getHeader rather than _header

---
 lib/response.js       |  7 +++----
 test/index.test.js    | 21 ++++-----------------
 types/index.d.ts      |  2 +-
 types/index.test-d.ts | 29 ++++++++++++++++-------------
 4 files changed, 24 insertions(+), 35 deletions(-)

diff --git a/lib/response.js b/lib/response.js
index 9db10f1..4a6945a 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -170,10 +170,9 @@ class Response extends http.ServerResponse {
 
     // Add raw headers
     ;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
-      const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
-      const field = this._header.match(regex)
-      if (field) {
-        this._lightMyRequest.headers[name.toLowerCase()] = field[1]
+      const value = this.getHeader(name)
+      if (value) {
+        this._lightMyRequest.headers[name.toLowerCase()] = value
       }
     })
   }
diff --git a/test/index.test.js b/test/index.test.js
index 43d3583..8633501 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -28,7 +28,7 @@ const httpMethods = [
 ]
 
 test('returns non-chunked payload', (t) => {
-  t.plan(7)
+  t.plan(6)
   const output = 'example.com:8080|/hello'
 
   const dispatch = function (req, res) {
@@ -42,10 +42,7 @@ test('returns non-chunked payload', (t) => {
     t.error(err)
     t.equal(res.statusCode, 200)
     t.equal(res.statusMessage, 'Super')
-    t.ok(res.headers.date)
     t.strictSame(res.headers, {
-      date: res.headers.date,
-      connection: 'keep-alive',
       'x-extra': 'hello',
       'content-type': 'text/plain',
       'content-length': output.length.toString()
@@ -56,7 +53,7 @@ test('returns non-chunked payload', (t) => {
 })
 
 test('returns single buffer payload', (t) => {
-  t.plan(6)
+  t.plan(3)
   const dispatch = function (req, res) {
     res.writeHead(200, { 'Content-Type': 'text/plain' })
     res.end(req.headers.host + '|' + req.url)
@@ -64,9 +61,6 @@ test('returns single buffer payload', (t) => {
 
   inject(dispatch, { url: 'http://example.com:8080/hello' }, (err, res) => {
     t.error(err)
-    t.ok(res.headers.date)
-    t.ok(res.headers.connection)
-    t.equal(res.headers['transfer-encoding'], 'chunked')
     t.equal(res.payload, 'example.com:8080|/hello')
     t.equal(res.rawPayload.toString(), 'example.com:8080|/hello')
   })
@@ -368,7 +362,7 @@ test('includes default https port in host header', (t) => {
 })
 
 test('optionally accepts an object as url', (t) => {
-  t.plan(5)
+  t.plan(3)
   const output = 'example.com:8080|/hello?test=1234'
 
   const dispatch = function (req, res) {
@@ -388,8 +382,6 @@ test('optionally accepts an object as url', (t) => {
 
   inject(dispatch, { url }, (err, res) => {
     t.error(err)
-    t.ok(res.headers.date)
-    t.ok(res.headers.connection)
     t.notOk(res.headers['transfer-encoding'])
     t.equal(res.payload, output)
   })
@@ -409,7 +401,7 @@ test('leaves user-agent unmodified', (t) => {
 })
 
 test('returns chunked payload', (t) => {
-  t.plan(5)
+  t.plan(2)
   const dispatch = function (req, res) {
     res.writeHead(200, 'OK')
     res.write('a')
@@ -419,9 +411,6 @@ test('returns chunked payload', (t) => {
 
   inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {
     t.error(err)
-    t.ok(res.headers.date)
-    t.ok(res.headers.connection)
-    t.equal(res.headers['transfer-encoding'], 'chunked')
     t.equal(res.payload, 'ab')
   })
 })
@@ -1640,8 +1629,6 @@ test('correctly handles no string headers', (t) => {
       date: date.toString(),
       true: 'true',
       false: 'false',
-      connection: 'keep-alive',
-      'transfer-encoding': 'chunked',
       'content-type': 'application/json'
     })
 
diff --git a/types/index.d.ts b/types/index.d.ts
index b267026..4abe499 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -14,7 +14,7 @@ declare namespace inject {
 
   export type DispatchFunc = http.RequestListener
 
-  export type CallbackFunc = (err: Error, response: Response) => void
+  export type CallbackFunc = (err: Error | undefined, response: Response | undefined) => void
 
   export type InjectPayload = string | object | Buffer | NodeJS.ReadableStream
 
diff --git a/types/index.test-d.ts b/types/index.test-d.ts
index 72fa8a0..4ff3b20 100644
--- a/types/index.test-d.ts
+++ b/types/index.test-d.ts
@@ -15,7 +15,10 @@ const dispatch: http.RequestListener = function (req, res) {
   res.end(reply)
 }
 
-const expectResponse = function (res: Response) {
+const expectResponse = function (res: Response | undefined) {
+  if (!res) {
+    return;
+  }
   expectType<Response>(res)
   console.log(res.payload)
   expectAssignable<Function>(res.json)
@@ -38,7 +41,7 @@ const expectResponse = function (res: Response) {
 expectType<DispatchFunc>(dispatch)
 
 inject(dispatch, { method: 'get', url: '/' }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
@@ -52,37 +55,37 @@ const url = {
   }
 }
 inject(dispatch, { method: 'get', url }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'get', url: '/', cookies: { name1: 'value1', value2: 'value2' } }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'get', url: '/', query: { name1: 'value1', value2: 'value2' } }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'get', url: '/', query: { name1: ['value1', 'value2'] } }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'get', url: '/', query: 'name1=value1' }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'post', url: '/', payload: { name1: 'value1', value2: 'value2' } }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
 inject(dispatch, { method: 'post', url: '/', body: { name1: 'value1', value2: 'value2' } }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })
 
@@ -90,9 +93,9 @@ expectType<void>(
   inject(dispatch)
     .get('/')
     .end((err, res) => {
-      expectType<Error>(err)
-      expectType<Response>(res)
-      console.log(res.payload)
+      expectType<Error | undefined>(err)
+      expectType<Response | undefined>(res)
+      console.log(res?.payload)
     })
 )
 
@@ -129,6 +132,6 @@ const httpDispatch = function (req: http.IncomingMessage, res: http.ServerRespon
 }
 
 inject(httpDispatch, { method: 'get', url: '/' }, (err, res) => {
-  expectType<Error>(err)
+  expectType<Error | undefined>(err)
   expectResponse(res)
 })

From fe834341356fd68eac8a896a67abf1673ef08066 Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 04:54:31 +0300
Subject: [PATCH 5/9] add 'use strict'

---
 lib/Chain.js    | 2 ++
 lib/doInject.js | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/lib/Chain.js b/lib/Chain.js
index 68aa4b2..854421a 100644
--- a/lib/Chain.js
+++ b/lib/Chain.js
@@ -1,3 +1,5 @@
+'use strict'
+
 const doInject = require('./doInject')
 
 const errorMessage = 'The dispatch function has already been invoked'
diff --git a/lib/doInject.js b/lib/doInject.js
index 925f8c3..6a17279 100644
--- a/lib/doInject.js
+++ b/lib/doInject.js
@@ -1,3 +1,5 @@
+'use strict'
+
 const assert = require('assert')
 const optsValidator = require('./configValidator')
 const Request = require('./request')

From 700d3739a1632c649441a3325bb6065c5f6d86ff Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 05:03:46 +0300
Subject: [PATCH 6/9] rename to kebab

---
 index.js                          | 4 ++--
 lib/{Chain.js => chain.js}        | 2 +-
 lib/{doInject.js => do-inject.js} | 0
 3 files changed, 3 insertions(+), 3 deletions(-)
 rename lib/{Chain.js => chain.js} (98%)
 rename lib/{doInject.js => do-inject.js} (100%)

diff --git a/index.js b/index.js
index aa5fe43..ee8ec15 100644
--- a/index.js
+++ b/index.js
@@ -2,8 +2,8 @@
 
 const Request = require('./lib/request')
 const Response = require('./lib/response')
-const Chain = require('./lib/Chain')
-const doInject = require('./lib/doInject')
+const Chain = require('./lib/chain')
+const doInject = require('./lib/do-inject')
 
 function inject (dispatchFunc, options, callback) {
   if (typeof callback === 'undefined') {
diff --git a/lib/Chain.js b/lib/chain.js
similarity index 98%
rename from lib/Chain.js
rename to lib/chain.js
index 854421a..fd21335 100644
--- a/lib/Chain.js
+++ b/lib/chain.js
@@ -1,6 +1,6 @@
 'use strict'
 
-const doInject = require('./doInject')
+const doInject = require('./do-inject')
 
 const errorMessage = 'The dispatch function has already been invoked'
 
diff --git a/lib/doInject.js b/lib/do-inject.js
similarity index 100%
rename from lib/doInject.js
rename to lib/do-inject.js

From 757708a4ebbc92fb432703da15460c539b697fb8 Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Thu, 31 Aug 2023 05:20:27 +0300
Subject: [PATCH 7/9] fix benchmark Custom Request

---
 lib/request.js    | 30 ++++++++++--------------------
 test/benchmark.js | 10 ++++++----
 2 files changed, 16 insertions(+), 24 deletions(-)

diff --git a/lib/request.js b/lib/request.js
index 0ea7b65..aeaf474 100644
--- a/lib/request.js
+++ b/lib/request.js
@@ -4,7 +4,6 @@
 
 const { Readable, addAbortSignal } = require('stream')
 const cookie = require('cookie')
-const util = require('util')
 const assert = require('assert')
 const warning = require('process-warning')()
 
@@ -214,31 +213,22 @@ class Request extends Readable {
   }
 }
 
-function Classes (bases) {
-  class _CustomLMRRequest {
-    constructor (...opt) {
-      bases.forEach(Base => {
-        Object.assign(this, new Base(...opt))
-      })
-
-      util.inherits(this.constructor, bases[bases.length - 1])
-    }
-  }
-  bases.forEach(base => {
-    Object.getOwnPropertyNames(base.prototype)
-      .filter(prop => prop !== 'constructor')
-      .forEach(prop => { _CustomLMRRequest.prototype[prop] = base.prototype[prop] })
-  })
-  return _CustomLMRRequest
-}
-
 /**
  * @template T
  * @param {new (opt: import('../types').InjectOptions) => T} CustomRequest
  * @returns {new (opt: import('../types').InjectOptions) => T & Request}
  */
 function getCustomRequest (CustomRequest) {
-  return Classes([Request, CustomRequest])
+  class _CustomLMRRequest extends CustomRequest {
+    constructor (...opt) {
+      super(...opt)
+      Object.assign(this, new Request(...opt))
+    }
+  }
+  Object.getOwnPropertyNames(Request.prototype)
+    .filter(prop => prop !== 'constructor')
+    .forEach(prop => { _CustomLMRRequest.prototype[prop] = Request.prototype[prop] })
+  return _CustomLMRRequest
 }
 
 module.exports = Request
diff --git a/test/benchmark.js b/test/benchmark.js
index 6e52182..7f4d4b8 100644
--- a/test/benchmark.js
+++ b/test/benchmark.js
@@ -26,6 +26,7 @@ const mockCustomReq = {
   },
   Request: http.IncomingMessage
 }
+
 const mockReqCookies = {
   url: 'http://localhost',
   method: 'GET',
@@ -52,11 +53,12 @@ const mockReqCookiesPayload = {
   }
 }
 
-suite.add('Request', function () {
-  new Request(mockReq)
-})
+suite
+  .add('Request', function () {
+    new Request(mockReq)
+  })
   .add('Custom Request', function () {
-    new Request.CustomRequest(mockCustomReq)
+    new (Request.getCustomRequest(mockCustomReq.Request))(mockCustomReq)
   })
   .add('Request With Cookies', function () {
     new Request(mockReqCookies)

From e5d4b0cb382e7611fe2f87bc67c6d430ff3affde Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Wed, 13 Sep 2023 14:24:12 +0300
Subject: [PATCH 8/9] fix Custom Request

---
 benchmark/benchmark.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/benchmark/benchmark.js b/benchmark/benchmark.js
index 4267802..a0d6722 100644
--- a/benchmark/benchmark.js
+++ b/benchmark/benchmark.js
@@ -86,7 +86,7 @@ suite
     new Request(mockReq) // eslint-disable-line no-new
   })
   .add('Custom Request', function () {
-    new Request.CustomRequest(mockCustomReq) // eslint-disable-line no-new
+    new (Request.getCustomRequest(mockCustomReq.Request))(mockCustomReq) // eslint-disable-line no-new
   })
   .add('Request With Cookies', function () {
     new Request(mockReqCookies) // eslint-disable-line no-new

From 2bda23474a8f65b23b61d6c9ec8ec8ca63202ef5 Mon Sep 17 00:00:00 2001
From: Vlad Sirenko <sirenkovladd@gmail.com>
Date: Tue, 24 Oct 2023 02:15:40 -0700
Subject: [PATCH 9/9] return copy header from ServerResponse._header

---
 lib/response.js    |  5 ++++-
 test/index.test.js | 21 +++++++++++++++++----
 2 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/lib/response.js b/lib/response.js
index c226bb5..3cbe5eb 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -170,7 +170,10 @@ class Response extends http.ServerResponse {
 
     // Add raw headers
     ;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
-      const value = this.getHeader(name)
+      // TODO change this to use the header getter
+      const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
+      const [, value] = this._header.match(regex) || []
+      // const value = this.getHeader(name)
       if (value) {
         this._lightMyRequest.headers[name.toLowerCase()] = value
       }
diff --git a/test/index.test.js b/test/index.test.js
index 04e9afd..35d61bb 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -28,7 +28,7 @@ const httpMethods = [
 ]
 
 test('returns non-chunked payload', (t) => {
-  t.plan(6)
+  t.plan(7)
   const output = 'example.com:8080|/hello'
 
   const dispatch = function (req, res) {
@@ -42,7 +42,10 @@ test('returns non-chunked payload', (t) => {
     t.error(err)
     t.equal(res.statusCode, 200)
     t.equal(res.statusMessage, 'Super')
+    t.ok(res.headers.date)
     t.strictSame(res.headers, {
+      date: res.headers.date,
+      connection: 'keep-alive',
       'x-extra': 'hello',
       'content-type': 'text/plain',
       'content-length': output.length.toString()
@@ -53,7 +56,7 @@ test('returns non-chunked payload', (t) => {
 })
 
 test('returns single buffer payload', (t) => {
-  t.plan(3)
+  t.plan(6)
   const dispatch = function (req, res) {
     res.writeHead(200, { 'Content-Type': 'text/plain' })
     res.end(req.headers.host + '|' + req.url)
@@ -61,6 +64,9 @@ test('returns single buffer payload', (t) => {
 
   inject(dispatch, { url: 'http://example.com:8080/hello' }, (err, res) => {
     t.error(err)
+    t.ok(res.headers.date)
+    t.ok(res.headers.connection)
+    t.equal(res.headers['transfer-encoding'], 'chunked')
     t.equal(res.payload, 'example.com:8080|/hello')
     t.equal(res.rawPayload.toString(), 'example.com:8080|/hello')
   })
@@ -362,7 +368,7 @@ test('includes default https port in host header', (t) => {
 })
 
 test('optionally accepts an object as url', (t) => {
-  t.plan(3)
+  t.plan(5)
   const output = 'example.com:8080|/hello?test=1234'
 
   const dispatch = function (req, res) {
@@ -382,6 +388,8 @@ test('optionally accepts an object as url', (t) => {
 
   inject(dispatch, { url }, (err, res) => {
     t.error(err)
+    t.ok(res.headers.date)
+    t.ok(res.headers.connection)
     t.notOk(res.headers['transfer-encoding'])
     t.equal(res.payload, output)
   })
@@ -401,7 +409,7 @@ test('leaves user-agent unmodified', (t) => {
 })
 
 test('returns chunked payload', (t) => {
-  t.plan(2)
+  t.plan(5)
   const dispatch = function (req, res) {
     res.writeHead(200, 'OK')
     res.write('a')
@@ -411,6 +419,9 @@ test('returns chunked payload', (t) => {
 
   inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {
     t.error(err)
+    t.ok(res.headers.date)
+    t.ok(res.headers.connection)
+    t.equal(res.headers['transfer-encoding'], 'chunked')
     t.equal(res.payload, 'ab')
   })
 })
@@ -1629,6 +1640,8 @@ test('correctly handles no string headers', (t) => {
       date: date.toString(),
       true: 'true',
       false: 'false',
+      connection: 'keep-alive',
+      'transfer-encoding': 'chunked',
       'content-type': 'application/json'
     })