Home Reference Source

lib/network/Response.js

const { STATUS_CODES } = require('http')
const util = require('util')
const SecurityDetails = require('./SecurityDetails')
const {
  NonHTTP2Protocols,
  CRLF,
  HTTP11,
  SpaceChar,
  stringifyHeaders,
  headersToLowerCase
} = require('./_shared')

class Response {
  /**
   * @param {Object} client
   * @param {?Request} request
   * @param {Object} event
   */
  constructor (client, request, event) {
    /**
     * @type {Object}
     * @protected
     */
    this._client = client

    /**
     * @type {?Request}
     * @protected
     */
    this._request = request

    /**
     * @type {string}
     * @protected
     */
    this._requestId = request != null ? request._requestId : event.requestId

    /**
     * @type {string}
     * @protected
     */
    this._loaderId = event.loaderId

    /**
     * @type {number}
     * @protected
     */
    this._timestamp = event.timestamp

    /**
     * @type {string}
     * @protected
     */
    this._type = event.type

    /**
     * @type {string}
     * @protected
     */
    this._frameId = event.frameId

    /** @type {Object} **/
    const rinfo =
      event.redirectResponse != null ? event.redirectResponse : event.response

    /**
     * @type {string}
     * @protected
     */
    this._url = rinfo.url

    /**
     * @type {?URL}
     * @private
     */
    this._purl = null

    /**
     * @type {?Object}
     * @protected
     */
    this._requestHeaders = rinfo.requestHeaders

    /**
     * @type {?string}
     * @protected
     */
    this._requestHeadersText = rinfo.requestHeadersText

    /**
     * @type {Object}
     * @protected
     */
    this._headers = rinfo.headers

    /**
     * @type {?string}
     * @protected
     */
    this._headersText = rinfo.headersText

    /**
     * @type {number}
     * @protected
     */
    this._status = rinfo.status

    /**
     * @type {string}
     * @protected
     */
    this._statusText = !rinfo.statusText
      ? STATUS_CODES[this._status]
      : rinfo.statusText

    /**
     * @type {string}
     * @protected
     */
    this._protocol = rinfo.protocol ? rinfo.protocol.toUpperCase() : HTTP11

    if (this._request) {
      this._request._protocol = this._protocol
      this._request._fullHeaders = this._requestHeaders
      this._request._headersText = this._requestHeadersText
    }

    /**
     * @type {boolean}
     * @protected
     */
    this._fromDiskCache = !!rinfo.fromDiskCache

    /**
     * @type {boolean}
     * @protected
     */
    this._fromServiceWorker = !!rinfo.fromServiceWorker

    /**
     * @type {string}
     * @protected
     */
    this._mimeType = rinfo.mimeType

    /**
     * @type {string}
     * @protected
     */
    this._remoteIPAddress = rinfo.remoteIPAddress

    /**
     * @type {number}
     * @protected
     */
    this._remotePort = rinfo.remotePort

    /**
     * @type {number}
     * @protected
     */
    this._encodedDataLength = rinfo.encodedDataLength

    /**
     * @type {boolean}
     * @protected
     */
    this._connectionReused = rinfo.connectionReused

    /**
     * @type {Object}
     * @private
     */
    this._timing = rinfo.timing

    /**
     * @type {?string}
     */
    this._securityState = rinfo.securityState

    /** @type {?SecurityDetails} */
    this._securityDetails = null
    if (rinfo.securityDetails) {
      this._securityDetails = new SecurityDetails(rinfo.securityDetails)
    }

    /**
     * @type {?function()}
     */
    this._bodyLoadedPromiseFulfill = null
    this._bodyLoadedPromise = new Promise(resolve => {
      this._bodyLoadedPromiseFulfill = resolve
    })

    /**
     * @type {Promise<Buffer>}
     */
    this._contentPromise = null

    /** @type {?Object} */
    this._headersLower = null
  }

  /**
   * @return {boolean}
   */
  ok () {
    return this._status === 0 || (this._status >= 200 && this._status <= 299)
  }

  /**
   * @param {boolean} [noHTTP2Plus] - When true if the request was made via HTTP2 the protocol is forced to HTTP/1.1
   * @return {string}
   */
  statusLine (noHTTP2Plus) {
    let proto = this._protocol
    if (noHTTP2Plus && !NonHTTP2Protocols.has(proto)) {
      proto = HTTP11
    }
    return `${proto} ${this._status} ${this.statusText()}`
  }

  /**
   * @param {boolean} [noHTTP2Plus] - When true if the request was made via HTTP2 the protocol is forced to HTTP/1.1
   * @return {string}
   */
  statusLineAndHeaders (noHTTP2Plus) {
    if (!noHTTP2Plus) {
      if (this._headersText) return this._headersText
      return `${this.statusLine()}${CRLF}${stringifyHeaders(this._headers)}`
    }
    if (this._headersText) {
      const protocol = this._headersText.substring(
        0,
        this._headersText.indexOf(SpaceChar)
      )
      if (NonHTTP2Protocols.has(protocol)) {
        return this._headersText
      }
      return this._headersText.replace(protocol, HTTP11)
    }
    return `${this.statusLine(noHTTP2Plus)}${CRLF}${stringifyHeaders(
      this._headers
    )}`
  }

  /**
   *
   * @return {string}
   */
  url () {
    return this._url
  }

  /**
   * @return {URL}
   */
  parsedURL () {
    if (!this._purl) {
      this._purl = new URL(this.url())
    }
    return this._purl
  }

  /**
   *
   * @return {string}
   */
  protocol () {
    return this._protocol
  }

  /**
   *
   * @return {Object}
   */
  headers () {
    return this._headers
  }

  /**
   * Returns the normalized (header keys lowercase) HTTP headers as sent by the browser.
   * @return {Object}
   */
  normalizedHeaders () {
    if (!this._headersLower) {
      this._headersLower = headersToLowerCase(this.headers())
    }
    return this._headersLower
  }

  /**
   * Returns the value for the supplied header (case insensitive) if it exists
   * @param {string} header
   * @return {?string}
   */
  header (header) {
    const headers = this.normalizedHeaders()
    if (headers) return headers[header.toLowerCase()]
    return null
  }

  /**
   * Returns the responses (status line and headers with CRLF) as sent by the browser if they were included with the response
   * @return {?string}
   */
  headersText () {
    return this._headersText
  }

  /**
   * Returns the full request (the one generating this response) HTTP headers as sent by the browser if they were included with the response
   * @return {?Object}
   */
  requestHeaders () {
    return this._requestHeaders
  }

  /**
   * Returns the full request's (the one generating this response) HTTP headers (request line and headers with CRLF) as sent by the browser if they were included with the response
   * @return {?string}
   */
  requestHeadersText () {
    return this._requestHeadersText
  }

  /**
   *
   * @return {number}
   */
  status () {
    return this._status
  }

  /**
   *
   * @return {string}
   */
  statusText () {
    return this._statusText || STATUS_CODES[this._status]
  }

  /**
   *
   * @return {string}
   */
  type () {
    return this._type
  }

  /**
   *
   * @return {number}
   */
  encodedDataLength () {
    return this._encodedDataLength
  }

  /**
   *
   * @return {string}
   */
  frameId () {
    return this._frameId
  }

  /**
   *
   * @return {boolean}
   */
  fromDiskCache () {
    return this._fromDiskCache
  }

  /**
   * @return {boolean}
   */
  fromCache () {
    return this._fromDiskCache || this._request.fromMemoryCache()
  }

  /**
   *
   * @return {boolean}
   */
  fromServiceWorker () {
    return this._fromServiceWorker
  }

  /**
   *
   * @return {string}
   */
  mimeType () {
    return this._mimeType
  }

  /**
   *
   * @return {string}
   */
  remoteIPAddress () {
    return this._remoteIPAddress
  }

  /**
   *
   * @return {number}
   */
  remotePort () {
    return this._remotePort
  }

  /**
   * @return {{port: number, ip: string}}
   */
  remoteAddress () {
    return { ip: this._remoteIPAddress, port: this._remotePort }
  }

  /**
   *
   * @return {boolean}
   */
  connectionReused () {
    return this._connectionReused
  }

  /**
   *
   * @return {Object}
   */
  timing () {
    return this._timing
  }

  /**
   * @return {?string}
   */
  securityState () {
    return this._securityState
  }

  /**
   * @return {?SecurityDetails}
   */
  securityDetails () {
    return this._securityDetails
  }

  /**
   *
   * @return {string}
   */
  requestId () {
    return this._requestId
  }

  /**
   *
   * @return {string}
   */
  loaderId () {
    return this._loaderId
  }

  /**
   *
   * @return {string}
   */
  documentURL () {
    return this._request.documentURL()
  }

  /**
   *
   * @return {number}
   */
  timestamp () {
    return this._timestamp
  }

  /**
   * @return {?Request}
   */
  request () {
    return this._request
  }

  /**
   * @return {?Frame}
   */
  frame () {
    return this._request.frame()
  }

  /**
   * @return {Promise<Buffer>}
   */
  buffer () {
    if (!this._contentPromise) {
      this._contentPromise = this._bodyLoadedPromise.then(async error => {
        if (error) throw error
        const response = await this._client.send('Network.getResponseBody', {
          requestId: this._request._requestId
        })
        return Buffer.from(
          response.body,
          response.base64Encoded ? 'base64' : 'utf8'
        )
      })
    }
    return this._contentPromise
  }

  /**
   * @return {Promise<string>}
   */
  async text () {
    const content = await this.buffer()
    return content.toString('utf8')
  }

  /**
   * @return {Promise<Object>}
   */
  async json () {
    const content = await this.text()
    return JSON.parse(content)
  }

  /**
   * @return {{headers: Object, securityDetails: SecurityDetails, frameId: string, connectionReused: boolean, timing: Object, loaderId: string, encodedDataLength: number, remotePort: number, mimeType: string, type: string, headersText: ?string, securityState: string, url: string, requestHeadersText: ?string, protocol: string, requestHeaders: ?Object, fromDiskCache: boolean, fromServiceWorker: boolean, remoteIPAddress: string, requestId: string, statusText: string, timestamp: number, status: number}}
   */
  toJSON () {
    return {
      requestId: this._requestId,
      loaderId: this._loaderId,
      timestamp: this._timestamp,
      type: this._type,
      frameId: this._frameId,
      url: this._url,
      requestHeaders: this._requestHeaders,
      requestHeadersText: this._requestHeadersText,
      headers: this._headers,
      headersText: this._headersText,
      status: this._status,
      statusText: this._statusText,
      protocol: this._protocol,
      fromDiskCache: this._fromDiskCache,
      fromServiceWorker: this._fromServiceWorker,
      mimeType: this._mimeType,
      remoteIPAddress: this._remoteIPAddress,
      remotePort: this._remotePort,
      encodedDataLength: this._encodedDataLength,
      connectionReused: this._connectionReused,
      timing: this._timing,
      securityState: this._securityState,
      securityDetails: this._securityDetails
    }
  }

  /** @ignore */
  // eslint-disable-next-line space-before-function-paren
  [util.inspect.custom](depth, options) {
    if (depth < 0) {
      return options.stylize('[Response]', 'special')
    }

    const newOptions = Object.assign({}, options, {
      depth: options.depth == null ? null : options.depth - 1
    })
    const inner = util.inspect(
      {
        url: this._url,
        type: this._type,
        requestId: this._requestId,
        frameId: this._frameId,
        headers: this._headers,
        status: this._status,
        statusText: this.statusText(),
        protocol: this._protocol,
        mime: this._mimeType
      },
      newOptions
    )
    return `${options.stylize('Response', 'special')} ${inner}`
  }
}

/**
 * @type {Response}
 */
module.exports = Response