async request()

in src/connection/HttpConnection.ts [87:325]


  async request (params: ConnectionRequestParams, options: ConnectionRequestOptions): Promise<ConnectionRequestResponse>
  async request (params: ConnectionRequestParams, options: ConnectionRequestOptionsAsStream): Promise<ConnectionRequestResponseAsStream>
  async request (params: ConnectionRequestParams, options: any): Promise<any> {
    return await new Promise((resolve, reject) => {
      let cleanedListeners = false

      const maxResponseSize = options.maxResponseSize ?? MAX_STRING_LENGTH
      const maxCompressedResponseSize = options.maxCompressedResponseSize ?? MAX_BUFFER_LENGTH
      const requestParams = this.buildRequestObject(params, options)
      // https://github.com/nodejs/node/commit/b961d9fd83
      if (INVALID_PATH_REGEX.test(requestParams.path as string)) {
        return reject(new TypeError(`ERR_UNESCAPED_CHARACTERS: ${requestParams.path as string}`))
      }

      debug('Starting a new request', params)

      // tracking response.end, request.finish and the value of the returnable response object here is necessary:
      // we only know a request is truly finished when one of the following is true:
      // - request.finish and response.end have both fired (success)
      // - request.error has fired (failure)
      // - response.close has fired (failure)
      let responseEnded = false
      let requestFinished = false
      let connectionRequestResponse: ConnectionRequestResponse | ConnectionRequestResponseAsStream

      let request: http.ClientRequest
      try {
        request = this.makeRequest(requestParams)
      } catch (err: any) {
        return reject(err)
      }

      const abortListener = (): void => {
        request.destroy(new RequestAbortedError('Request aborted'))
      }

      this._openRequests++
      if (options.signal != null) {
        options.signal.addEventListener(
          'abort',
          abortListener,
          { once: true }
        )
      }

      const onResponse = (response: http.IncomingMessage): void => {
        cleanListeners()

        if (options.asStream === true) {
          return resolve({
            body: response,
            statusCode: response.statusCode as number,
            headers: response.headers
          })
        }

        const contentEncoding = (response.headers['content-encoding'] ?? '').toLowerCase()
        const isCompressed = contentEncoding.includes('gzip') || contentEncoding.includes('deflate')
        const bodyIsBinary = isBinary(response.headers['content-type'] ?? '')

        /* istanbul ignore else */
        if (response.headers['content-length'] !== undefined) {
          const contentLength = Number(response.headers['content-length'])
          if (isCompressed && contentLength > maxCompressedResponseSize) {
            response.destroy()
            return reject(
              new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed buffer (${maxCompressedResponseSize})`)
            )
          } else if (contentLength > maxResponseSize) {
            response.destroy()
            return reject(
              new RequestAbortedError(`The content length (${contentLength}) is bigger than the maximum allowed string (${maxResponseSize})`)
            )
          }
        }

        // if the response is compressed, we must handle it
        // as buffer for allowing decompression later
        let payload = isCompressed || bodyIsBinary ? new Array<Buffer>() : ''
        const onData = isCompressed || bodyIsBinary ? onDataAsBuffer : onDataAsString

        let currentLength = 0
        function onDataAsBuffer (chunk: Buffer): void {
          currentLength += Buffer.byteLength(chunk)
          if (currentLength > maxCompressedResponseSize) {
            response.destroy(new RequestAbortedError(`The content length (${currentLength}) is bigger than the maximum allowed buffer (${maxCompressedResponseSize})`))
          } else {
            (payload as Buffer[]).push(chunk)
          }
        }

        function onDataAsString (chunk: string): void {
          currentLength += Buffer.byteLength(chunk)
          if (currentLength > maxResponseSize) {
            response.destroy(new RequestAbortedError(`The content length (${currentLength}) is bigger than the maximum allowed string (${maxResponseSize})`))
          } else {
            payload = `${payload as string}${chunk}`
          }
        }

        const onEnd = (): void => {
          response.removeListener('data', onData)
          response.removeListener('end', onEnd)

          responseEnded = true

          connectionRequestResponse = {
            body: isCompressed || bodyIsBinary ? Buffer.concat(payload as Buffer[]) : payload as string,
            statusCode: response.statusCode as number,
            headers: response.headers
          }

          if (requestFinished) {
            return resolve(connectionRequestResponse)
          }
        }

        const onResponseClose = (): void => {
          return reject(new ConnectionError('Response aborted while reading the body'))
        }

        if (!isCompressed && !bodyIsBinary) {
          response.setEncoding('utf8')
        }

        this.diagnostic.emit('deserialization', null, options)
        response.on('data', onData)
        response.on('end', onEnd)
        response.on('close', onResponseClose)
      }

      const onTimeout = (): void => {
        cleanListeners()
        request.once('error', noop) // we need to catch the request aborted error
        request.destroy()
        return reject(new TimeoutError('Request timed out'))
      }

      const onError = (err: Error): void => {
        // @ts-expect-error
        let { name, message, code } = err

        // ignore this error, it means we got a response body for a request that didn't expect a body (e.g. HEAD)
        // rather than failing, let it return a response with an empty string as body
        if (code === 'HPE_INVALID_CONSTANT' && message.startsWith('Parse Error: Expected HTTP/')) return

        cleanListeners()
        if (name === 'RequestAbortedError') {
          return reject(err)
        }

        if (code === 'ECONNRESET') {
          message += ` - Local: ${request.socket?.localAddress ?? 'unknown'}:${request.socket?.localPort ?? 'unknown'}, Remote: ${request.socket?.remoteAddress ?? 'unknown'}:${request.socket?.remotePort ?? 'unknown'}`
        } else if (code === 'EPIPE') {
          message = 'Response aborted while reading the body'
        }
        return reject(new ConnectionError(message))
      }

      const onSocket = (socket: TLSSocket): void => {
        /* istanbul ignore else */
        if (!socket.isSessionReused()) {
          socket.once('secureConnect', () => {
            const issuerCertificate = getIssuerCertificate(socket)
            /* istanbul ignore next */
            if (issuerCertificate == null) {
              onError(new Error('Invalid or malformed certificate'))
              request.once('error', noop) // we need to catch the request aborted error
              return request.destroy()
            }

            // Check if fingerprint matches
            /* istanbul ignore else */
            if (!isCaFingerprintMatch(this[kCaFingerprint], issuerCertificate.fingerprint256)) {
              onError(new Error('Server certificate CA fingerprint does not match the value configured in caFingerprint'))
              request.once('error', noop) // we need to catch the request aborted error
              return request.destroy()
            }
          })
        }
      }

      const onFinish = (): void => {
        requestFinished = true

        if (responseEnded) {
          if (connectionRequestResponse != null) {
            return resolve(connectionRequestResponse)
          } else {
            return reject(new Error('No response body received'))
          }
        }
      }

      const cleanListeners = (): void => {
        if (cleanedListeners) return

        this._openRequests--

        // we do NOT stop listening to request.error here
        // all errors we care about in the request/response lifecycle will bubble up to request.error, and may occur even after the request has been sent
        request.removeListener('response', onResponse)
        request.removeListener('timeout', onTimeout)
        request.removeListener('socket', onSocket)
        if (options.signal != null) {
          if ('removeEventListener' in options.signal) {
            options.signal.removeEventListener('abort', abortListener)
          } else {
            options.signal.removeListener('abort', abortListener)
          }
        }
        cleanedListeners = true
      }

      request.on('response', onResponse)
      request.on('timeout', onTimeout)
      request.on('error', onError)
      request.on('finish', onFinish)
      if (this[kCaFingerprint] != null && requestParams.protocol === 'https:') {
        request.on('socket', onSocket)
      }

      // Disables the Nagle algorithm
      request.setNoDelay(true)

      // starts the request
      if (isStream(params.body)) {
        pipeline(params.body, request, err => {
          /* istanbul ignore if  */
          if (err != null && !cleanedListeners) {
            cleanListeners()
            return reject(err)
          }
        })
      } else {
        request.end(params.body)
      }
    })
  }