OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / bare-http1 / lib / server-response.js

144 lines · javascript ref:
const { isFinishing } = require('bare-stream')
const HTTPOutgoingMessage = require('./outgoing-message')
const constants = require('./constants')

const CHUNK_DELIMITER = Buffer.from('\r\n')
const CHUNK_TERMINATOR = Buffer.from('0\r\n\r\n')

module.exports = class HTTPServerResponse extends HTTPOutgoingMessage {
  constructor(socket, req) {
    super(socket)

    this._req = req

    this._statusCode = 200
    this._statusMessage = null

    this._chunked = true
    this._close = req.headers.connection === 'close'
    this._onlyHeaders = req.method === 'HEAD'

    this._pendingWrite = null
  }

  get req() {
    return this._req
  }

  get statusCode() {
    return this._statusCode
  }

  set statusCode(value) {
    this._statusCode = value
  }

  get statusMessage() {
    return this._statusMessage
  }

  set statusMessage(value) {
    this._statusMessage = value
  }

  writeHead(statusCode, statusMessage = null, headers = {}) {
    if (typeof statusMessage === 'object' && statusMessage !== null) {
      headers = statusMessage
      statusMessage = null
    }

    this._statusCode = statusCode
    this._statusMessage = statusMessage || null

    if (headers) this._headers = { ...this._headers, ...headers }
  }

  _header() {
    let h =
      'HTTP/1.1 ' +
      this._statusCode +
      ' ' +
      (this._statusMessage === null ? constants.status[this._statusCode] : this._statusMessage) +
      '\r\n'

    for (const name of Object.keys(this._headers)) {
      const n = name.toLowerCase()
      const v = this._headers[name]

      if (n === 'content-length') this._chunked = false
      if (n === 'connection' && v && v.toLowerCase() === 'close') this._close = true

      h += httpCase(n) + ': ' + v + '\r\n'
    }

    if (this._chunked) h += 'Transfer-Encoding: chunked\r\n'

    h += '\r\n'

    return h
  }

  _write(data, encoding, cb) {
    if (this._headersSent === false) {
      if (isFinishing(this)) {
        this.setHeader(
          'Content-Length',
          (data.byteLength + this._writableState.buffered).toString()
        )
      }

      this.flushHeaders()
    }

    if (this._onlyHeaders === true) return cb(null)

    if (this._chunked) {
      this._socket.write(Buffer.from(data.byteLength.toString(16)))
      this._socket.write(CHUNK_DELIMITER)
    }

    let flushed = this._socket.write(data)

    if (this._chunked) flushed = this._socket.write(CHUNK_DELIMITER)

    if (flushed) cb(null)
    else this._pendingWrite = cb
  }

  _final(cb) {
    if (this._headersSent === false) {
      this.setHeader('Content-Length', '0')
      this.flushHeaders()
    }

    if (this._chunked && this._onlyHeaders === false) this._socket.write(CHUNK_TERMINATOR)

    if (this._close) this._socket.end()

    cb(null)
  }

  _predestroy() {
    super._predestroy()

    this._req.destroy()

    this._continueWrite()
  }

  _continueWrite() {
    if (this._pendingWrite === null) return
    const cb = this._pendingWrite
    this._pendingWrite = null
    cb(null)
  }
}

function httpCase(n) {
  let s = ''
  for (const part of n.split('-')) {
    s += (s ? '-' : '') + part.slice(0, 1).toUpperCase() + part.slice(1)
  }
  return s
}