h2c-client.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. 'use strict'
  2. const { connect } = require('node:net')
  3. const { kClose, kDestroy } = require('../core/symbols')
  4. const { InvalidArgumentError } = require('../core/errors')
  5. const util = require('../core/util')
  6. const Client = require('./client')
  7. const DispatcherBase = require('./dispatcher-base')
  8. class H2CClient extends DispatcherBase {
  9. #client = null
  10. constructor (origin, clientOpts) {
  11. super()
  12. if (typeof origin === 'string') {
  13. origin = new URL(origin)
  14. }
  15. if (origin.protocol !== 'http:') {
  16. throw new InvalidArgumentError(
  17. 'h2c-client: Only h2c protocol is supported'
  18. )
  19. }
  20. const { connect, maxConcurrentStreams, pipelining, ...opts } =
  21. clientOpts ?? {}
  22. let defaultMaxConcurrentStreams = 100
  23. let defaultPipelining = 100
  24. if (
  25. maxConcurrentStreams != null &&
  26. Number.isInteger(maxConcurrentStreams) &&
  27. maxConcurrentStreams > 0
  28. ) {
  29. defaultMaxConcurrentStreams = maxConcurrentStreams
  30. }
  31. if (pipelining != null && Number.isInteger(pipelining) && pipelining > 0) {
  32. defaultPipelining = pipelining
  33. }
  34. if (defaultPipelining > defaultMaxConcurrentStreams) {
  35. throw new InvalidArgumentError(
  36. 'h2c-client: pipelining cannot be greater than maxConcurrentStreams'
  37. )
  38. }
  39. this.#client = new Client(origin, {
  40. ...opts,
  41. connect: this.#buildConnector(connect),
  42. maxConcurrentStreams: defaultMaxConcurrentStreams,
  43. pipelining: defaultPipelining,
  44. allowH2: true
  45. })
  46. }
  47. #buildConnector (connectOpts) {
  48. return (opts, callback) => {
  49. const timeout = connectOpts?.connectOpts ?? 10e3
  50. const { hostname, port, pathname } = opts
  51. const socket = connect({
  52. ...opts,
  53. host: hostname,
  54. port,
  55. pathname
  56. })
  57. // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
  58. if (opts.keepAlive == null || opts.keepAlive) {
  59. const keepAliveInitialDelay =
  60. opts.keepAliveInitialDelay == null ? 60e3 : opts.keepAliveInitialDelay
  61. socket.setKeepAlive(true, keepAliveInitialDelay)
  62. }
  63. socket.alpnProtocol = 'h2'
  64. const clearConnectTimeout = util.setupConnectTimeout(
  65. new WeakRef(socket),
  66. { timeout, hostname, port }
  67. )
  68. socket
  69. .setNoDelay(true)
  70. .once('connect', function () {
  71. queueMicrotask(clearConnectTimeout)
  72. if (callback) {
  73. const cb = callback
  74. callback = null
  75. cb(null, this)
  76. }
  77. })
  78. .on('error', function (err) {
  79. queueMicrotask(clearConnectTimeout)
  80. if (callback) {
  81. const cb = callback
  82. callback = null
  83. cb(err)
  84. }
  85. })
  86. return socket
  87. }
  88. }
  89. dispatch (opts, handler) {
  90. return this.#client.dispatch(opts, handler)
  91. }
  92. async [kClose] () {
  93. await this.#client.close()
  94. }
  95. async [kDestroy] () {
  96. await this.#client.destroy()
  97. }
  98. }
  99. module.exports = H2CClient