current commit
260427.1 · fix: reuse parser WeakRef for timeout callbacks (#5125)
'use strict'

const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { Client, errors } = require('..')
const EventEmitter = require('node:events')
const { createServer } = require('node:http')
const { Readable } = require('node:stream')
const FakeTimers = require('@sinonjs/fake-timers')
const timers = require('../lib/util/timers')
const connectH1 = require('../lib/dispatcher/client-h1')
const {
  kMaxHeadersSize,
  kMaxResponseSize,
  kParser,
  kQueue,
  kRunningIdx
} = require('../lib/core/symbols')

class DummySocket extends EventEmitter {
  constructor () {
    super()
    this.destroyed = false
    this.errored = null
  }

  read () {
    return null
  }
}

test('parser reuses WeakRef when replacing timeout callbacks', async (t) => {
  const OriginalWeakRef = global.WeakRef
  t.after(() => {
    global.WeakRef = OriginalWeakRef
  })

  t = tspl(t, { plan: 1 })

  let weakRefCount = 0

  global.WeakRef = class CountingWeakRef extends OriginalWeakRef {
    constructor (target) {
      weakRefCount++
      super(target)
    }
  }

  const socket = new DummySocket()
  const client = {
    [kMaxHeadersSize]: 1024,
    [kMaxResponseSize]: 1024,
    [kQueue]: [],
    [kRunningIdx]: 0
  }

  await connectH1(client, socket)
  const parser = socket[kParser]

  parser.setTimeout(200, 0)
  parser.setTimeout(300, 0)
  parser.setTimeout(400, 1)
  parser.destroy()

  t.strictEqual(weakRefCount, 1)
})

test('refresh timeout on pause', async (t) => {
  t = tspl(t, { plan: 1 })

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.flushHeaders()
  })
  after(() => server.close())

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      bodyTimeout: 500
    })
    after(() => client.destroy())

    client.dispatch({
      path: '/',
      method: 'GET'
    }, {
      onRequestStart () {
      },
      onResponseStart (controller) {
        setTimeout(() => {
          controller.resume()
        }, 1000)
        controller.pause()
      },
      onResponseData () {

      },
      onResponseEnd () {

      },
      onResponseError (_controller, err) {
        t.ok(err instanceof errors.BodyTimeoutError)
      }
    })
  })

  await t.completed
})

test('start headers timeout after request body', async (t) => {
  t = tspl(t, { plan: 2 })

  const clock = FakeTimers.install({ shouldClearNativeTimers: true })
  after(() => clock.uninstall())

  const orgTimers = { ...timers }
  Object.assign(timers, { setTimeout, clearTimeout })
  after(() => {
    Object.assign(timers, orgTimers)
  })

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
  })
  after(() => server.close())

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      bodyTimeout: 0,
      headersTimeout: 100
    })
    after(() => client.destroy())

    const body = new Readable({ read () {} })
    client.dispatch({
      path: '/',
      body,
      method: 'GET'
    }, {
      onRequestStart () {
        process.nextTick(() => {
          clock.tick(200)
        })
        queueMicrotask(() => {
          body.push(null)
          body.on('end', () => {
            clock.tick(200)
          })
        })
      },
      onResponseStart () {
      },
      onResponseData () {

      },
      onResponseEnd () {

      },
      onResponseError (_controller, err) {
        t.equal(body.readableEnded, true)
        t.ok(err instanceof errors.HeadersTimeoutError)
      }
    })
  })

  await t.completed
})

test('start headers timeout after async iterator request body', async (t) => {
  t = tspl(t, { plan: 1 })

  const clock = FakeTimers.install({ shouldClearNativeTimers: true })
  after(() => clock.uninstall())

  const orgTimers = { ...timers }
  Object.assign(timers, { setTimeout, clearTimeout })
  after(() => {
    Object.assign(timers, orgTimers)
  })

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
  })
  after(() => server.close())

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      bodyTimeout: 0,
      headersTimeout: 100
    })
    after(() => client.destroy())
    let res
    const body = (async function * () {
      await new Promise((resolve) => { res = resolve })
      process.nextTick(() => {
        clock.tick(200)
      })
    })()
    client.dispatch({
      path: '/',
      body,
      method: 'GET'
    }, {
      onRequestStart () {
        process.nextTick(() => {
          clock.tick(200)
        })
        queueMicrotask(() => {
          res()
        })
      },
      onResponseStart () {
      },
      onResponseData () {

      },
      onResponseEnd () {

      },
      onResponseError (_controller, err) {
        t.ok(err instanceof errors.HeadersTimeoutError)
      }
    })
  })

  await t.completed
})

test('parser resume with no body timeout', async (t) => {
  t = tspl(t, { plan: 1 })

  const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
    res.end('asd')
  })
  after(() => server.close())

  server.listen(0, () => {
    const client = new Client(`http://localhost:${server.address().port}`, {
      bodyTimeout: 0
    })
    after(() => client.destroy())

    client.on('disconnect', () => {
      if (!client.closed && !client.destroyed) {
        t.fail('unexpected disconnect')
      }
    })

    client.dispatch({
      path: '/',
      method: 'GET'
    }, {
      onRequestStart () {
      },
      onResponseStart (controller) {
        setTimeout(() => controller.resume(), 2000)
        controller.pause()
      },
      onResponseData () {

      },
      onResponseEnd () {
        t.ok(true, 'pass')
      },
      onResponseError (_controller, err) {
        t.ifError(err)
      }
    })
  })

  await t.completed
})