loading
Generated 2023-04-29T00:49:34+00:00

All Files ( 95.18% covered at 46206.05 hits/line )

92 files in total.
6469 relevant lines, 6157 lines covered and 312 lines missed. ( 95.18% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/httpx.rb 100.00 % 74 38 38 0 855.76
lib/httpx/adapters/datadog.rb 79.39 % 269 131 104 27 40.35
lib/httpx/adapters/faraday.rb 95.04 % 266 141 134 7 54.57
lib/httpx/adapters/sentry.rb 100.00 % 116 60 60 0 62.67
lib/httpx/adapters/webmock.rb 100.00 % 142 76 76 0 122.01
lib/httpx/altsvc.rb 98.18 % 126 55 54 1 517.33
lib/httpx/buffer.rb 100.00 % 42 21 21 0 145623.24
lib/httpx/callbacks.rb 100.00 % 38 20 20 0 65965.75
lib/httpx/chainable.rb 90.63 % 89 32 29 3 1300.19
lib/httpx/connection.rb 95.59 % 687 363 347 16 63616.82
lib/httpx/connection/http1.rb 92.45 % 371 212 196 16 4934.49
lib/httpx/connection/http2.rb 96.11 % 415 257 247 10 82857.91
lib/httpx/domain_name.rb 100.00 % 148 44 44 0 290.34
lib/httpx/errors.rb 97.62 % 77 42 41 1 65.71
lib/httpx/extensions.rb 65.93 % 198 91 60 31 456126.78
lib/httpx/headers.rb 100.00 % 175 72 72 0 16046.18
lib/httpx/io.rb 100.00 % 7 5 5 0 26.00
lib/httpx/io/ssl.rb 93.55 % 156 62 58 4 1728.97
lib/httpx/io/tcp.rb 92.45 % 230 106 98 8 6697.55
lib/httpx/io/udp.rb 100.00 % 97 17 17 0 23469.06
lib/httpx/io/unix.rb 100.00 % 71 33 33 0 22.70
lib/httpx/loggable.rb 100.00 % 49 22 22 0 16178.00
lib/httpx/options.rb 89.19 % 310 148 132 16 24815.79
lib/httpx/parser/http1.rb 100.00 % 182 110 110 0 6818.55
lib/httpx/plugins/authentication.rb 100.00 % 20 7 7 0 37.71
lib/httpx/plugins/authentication/basic.rb 91.67 % 24 12 11 1 75.00
lib/httpx/plugins/authentication/digest.rb 98.36 % 102 61 60 1 118.31
lib/httpx/plugins/authentication/ntlm.rb 100.00 % 37 20 20 0 16.85
lib/httpx/plugins/authentication/socks5.rb 100.00 % 24 12 12 0 31.33
lib/httpx/plugins/aws_sdk_authentication.rb 100.00 % 106 45 45 0 23.56
lib/httpx/plugins/aws_sigv4.rb 100.00 % 218 102 102 0 127.39
lib/httpx/plugins/basic_authentication.rb 100.00 % 30 13 13 0 45.85
lib/httpx/plugins/circuit_breaker.rb 94.83 % 115 58 55 3 44.38
lib/httpx/plugins/circuit_breaker/circuit.rb 88.57 % 76 35 31 4 32.11
lib/httpx/plugins/circuit_breaker/circuit_store.rb 100.00 % 44 22 22 0 46.05
lib/httpx/plugins/compression.rb 100.00 % 165 83 83 0 251.84
lib/httpx/plugins/compression/brotli.rb 100.00 % 54 29 29 0 20.52
lib/httpx/plugins/compression/deflate.rb 100.00 % 54 31 31 0 82.71
lib/httpx/plugins/compression/gzip.rb 100.00 % 90 51 51 0 65.96
lib/httpx/plugins/cookies.rb 100.00 % 94 46 46 0 140.11
lib/httpx/plugins/cookies/cookie.rb 100.00 % 174 77 77 0 364.91
lib/httpx/plugins/cookies/jar.rb 100.00 % 97 47 47 0 263.21
lib/httpx/plugins/cookies/set_cookie_parser.rb 100.00 % 142 71 71 0 180.10
lib/httpx/plugins/digest_authentication.rb 100.00 % 62 29 29 0 96.55
lib/httpx/plugins/expect.rb 100.00 % 111 58 58 0 142.07
lib/httpx/plugins/follow_redirects.rb 97.06 % 138 68 66 2 107180.75
lib/httpx/plugins/grpc.rb 100.00 % 273 137 137 0 228.74
lib/httpx/plugins/grpc/call.rb 90.91 % 64 33 30 3 58.24
lib/httpx/plugins/grpc/message.rb 97.56 % 87 41 40 1 67.15
lib/httpx/plugins/h2c.rb 97.96 % 97 49 48 1 19.02
lib/httpx/plugins/multipart.rb 92.86 % 96 42 39 3 580.24
lib/httpx/plugins/multipart/decoder.rb 93.90 % 137 82 77 5 30.77
lib/httpx/plugins/multipart/encoder.rb 100.00 % 110 65 65 0 2305.31
lib/httpx/plugins/multipart/mime_type_detector.rb 92.11 % 78 38 35 3 194.29
lib/httpx/plugins/multipart/part.rb 100.00 % 34 18 18 0 532.39
lib/httpx/plugins/ntlm_authentication.rb 100.00 % 60 30 30 0 20.67
lib/httpx/plugins/persistent.rb 100.00 % 36 11 11 0 98.73
lib/httpx/plugins/proxy.rb 93.60 % 329 172 161 11 2362.17
lib/httpx/plugins/proxy/http.rb 100.00 % 176 106 106 0 1886.40
lib/httpx/plugins/proxy/socks4.rb 97.44 % 133 78 76 2 4816.10
lib/httpx/plugins/proxy/socks5.rb 98.21 % 192 112 110 2 5073.54
lib/httpx/plugins/proxy/ssh.rb 92.45 % 92 53 49 4 14.45
lib/httpx/plugins/push_promise.rb 100.00 % 81 41 41 0 15.61
lib/httpx/plugins/rate_limiter.rb 100.00 % 53 18 18 0 48.78
lib/httpx/plugins/response_cache.rb 97.67 % 178 86 84 2 74.86
lib/httpx/plugins/response_cache/store.rb 100.00 % 76 38 38 0 102.29
lib/httpx/plugins/retries.rb 97.92 % 197 96 94 2 79002.52
lib/httpx/plugins/stream.rb 94.44 % 151 72 68 4 98.32
lib/httpx/plugins/upgrade.rb 100.00 % 84 38 38 0 66.39
lib/httpx/plugins/upgrade/h2.rb 91.67 % 54 24 22 2 13.46
lib/httpx/plugins/webdav.rb 91.67 % 80 36 33 3 21.89
lib/httpx/pmatch_extensions.rb 100.00 % 33 17 17 0 14.47
lib/httpx/pool.rb 86.36 % 271 154 133 21 202971.74
lib/httpx/request.rb 96.20 % 295 158 152 6 3954.25
lib/httpx/resolver.rb 98.78 % 152 82 81 1 1314.28
lib/httpx/resolver/https.rb 86.90 % 245 145 126 19 25.26
lib/httpx/resolver/multi.rb 100.00 % 74 41 41 0 217936.49
lib/httpx/resolver/native.rb 89.92 % 442 258 232 26 94233.67
lib/httpx/resolver/resolver.rb 90.91 % 106 55 50 5 1063.40
lib/httpx/resolver/system.rb 89.92 % 209 119 107 12 19.75
lib/httpx/response.rb 100.00 % 372 201 201 0 1832.99
lib/httpx/selector.rb 88.41 % 140 69 61 8 586993.75
lib/httpx/session.rb 96.65 % 318 179 173 6 81021.26
lib/httpx/session_extensions.rb 100.00 % 26 11 11 0 7.91
lib/httpx/timers.rb 90.91 % 87 44 40 4 1210280.41
lib/httpx/transcoder.rb 100.00 % 92 52 52 0 300.79
lib/httpx/transcoder/body.rb 96.97 % 58 33 32 1 814.18
lib/httpx/transcoder/chunker.rb 100.00 % 115 66 66 0 273.29
lib/httpx/transcoder/form.rb 100.00 % 58 30 30 0 263.13
lib/httpx/transcoder/json.rb 100.00 % 59 33 33 0 32.70
lib/httpx/transcoder/xml.rb 92.86 % 54 28 26 2 44.04
lib/httpx/utils.rb 95.35 % 85 43 41 2 220668.88

lib/httpx.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "httpx/version"
  3. 26 require "httpx/extensions"
  4. 26 require "httpx/errors"
  5. 26 require "httpx/utils"
  6. 26 require "httpx/punycode"
  7. 26 require "httpx/domain_name"
  8. 26 require "httpx/altsvc"
  9. 26 require "httpx/callbacks"
  10. 26 require "httpx/loggable"
  11. 26 require "httpx/transcoder"
  12. 26 require "httpx/timers"
  13. 26 require "httpx/pool"
  14. 26 require "httpx/headers"
  15. 26 require "httpx/request"
  16. 26 require "httpx/response"
  17. 26 require "httpx/options"
  18. 26 require "httpx/chainable"
  19. 26 require "mutex_m"
  20. # Top-Level Namespace
  21. #
  22. 26 module HTTPX
  23. 26 EMPTY = [].freeze
  24. # All plugins should be stored under this module/namespace. Can register and load
  25. # plugins.
  26. #
  27. 26 module Plugins
  28. 26 @plugins = {}
  29. 26 @plugins.extend(Mutex_m)
  30. # Loads a plugin based on a name. If the plugin hasn't been loaded, tries to load
  31. # it from the load path under "httpx/plugins/" directory.
  32. #
  33. 26 def self.load_plugin(name)
  34. 7279 h = @plugins
  35. 14558 unless (plugin = h.synchronize { h[name] })
  36. 217 require "httpx/plugins/#{name}"
  37. 434 raise "Plugin #{name} hasn't been registered" unless (plugin = h.synchronize { h[name] })
  38. end
  39. 7279 plugin
  40. end
  41. # Registers a plugin (+mod+) in the central store indexed by +name+.
  42. #
  43. 26 def self.register_plugin(name, mod)
  44. 640 h = @plugins
  45. 1280 h.synchronize { h[name] = mod }
  46. end
  47. end
  48. skipped # :nocov:
  49. skipped def self.const_missing(const_name)
  50. skipped super unless const_name == :Client
  51. skipped warn "DEPRECATION WARNING: the class #{self}::Client is deprecated. Use #{self}::Session instead."
  52. skipped Session
  53. skipped end
  54. skipped # :nocov:
  55. 26 extend Chainable
  56. end
  57. 26 require "httpx/session"
  58. 26 require "httpx/session_extensions"
  59. # load integrations when possible
  60. 26 require "httpx/adapters/datadog" if defined?(DDTrace) || defined?(Datadog)
  61. 26 require "httpx/adapters/sentry" if defined?(Sentry)
  62. 26 require "httpx/adapters/webmock" if defined?(WebMock)

lib/httpx/adapters/datadog.rb

79.39% lines covered

131 relevant lines. 104 lines covered and 27 lines missed.
    
  1. # frozen_string_literal: true
  2. 8 if defined?(DDTrace) && DDTrace::VERSION::STRING >= "1.0.0"
  3. 8 require "datadog/tracing/contrib/integration"
  4. 8 require "datadog/tracing/contrib/configuration/settings"
  5. 8 require "datadog/tracing/contrib/patcher"
  6. 8 TRACING_MODULE = Datadog::Tracing
  7. else
  8. require "ddtrace/contrib/integration"
  9. require "ddtrace/contrib/configuration/settings"
  10. require "ddtrace/contrib/patcher"
  11. TRACING_MODULE = Datadog
  12. end
  13. 8 module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
  14. 8 module Contrib
  15. 8 module HTTPX
  16. 8 if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
  17. 8 METADATA_MODULE = TRACING_MODULE::Metadata
  18. 8 TYPE_OUTBOUND = TRACING_MODULE::Metadata::Ext::HTTP::TYPE_OUTBOUND
  19. 8 TAG_PEER_SERVICE = TRACING_MODULE::Metadata::Ext::TAG_PEER_SERVICE
  20. 8 TAG_URL = TRACING_MODULE::Metadata::Ext::HTTP::TAG_URL
  21. 8 TAG_METHOD = TRACING_MODULE::Metadata::Ext::HTTP::TAG_METHOD
  22. 8 TAG_TARGET_HOST = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_HOST
  23. 8 TAG_TARGET_PORT = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_PORT
  24. 8 TAG_STATUS_CODE = TRACING_MODULE::Metadata::Ext::HTTP::TAG_STATUS_CODE
  25. else
  26. METADATA_MODULE = Datadog
  27. TYPE_OUTBOUND = TRACING_MODULE::Ext::HTTP::TYPE_OUTBOUND
  28. TAG_PEER_SERVICE = TRACING_MODULE::Ext::Integration::TAG_PEER_SERVICE
  29. TAG_URL = TRACING_MODULE::Ext::HTTP::URL
  30. TAG_METHOD = TRACING_MODULE::Ext::HTTP::METHOD
  31. TAG_TARGET_HOST = TRACING_MODULE::Ext::NET::TARGET_HOST
  32. TAG_TARGET_PORT = TRACING_MODULE::Ext::NET::TARGET_PORT
  33. TAG_STATUS_CODE = Datadog::Ext::HTTP::STATUS_CODE
  34. PROPAGATOR = TRACING_MODULE::HTTPPropagator
  35. end
  36. # HTTPX Datadog Plugin
  37. #
  38. # Enables tracing for httpx requests. A span will be created for each individual requests,
  39. # and it'll trace since the moment it is fed to the connection, until the moment the response is
  40. # fed back to the session.
  41. #
  42. 8 module Plugin
  43. 8 class RequestTracer
  44. 8 include Contrib::HttpAnnotationHelper
  45. 8 SPAN_REQUEST = "httpx.request"
  46. 8 def initialize(request)
  47. 134 @request = request
  48. end
  49. 8 def call
  50. 134 return unless tracing_enabled?
  51. 134 @request.on(:response, &method(:finish))
  52. 134 verb = @request.verb
  53. 134 uri = @request.uri
  54. 134 @span = build_span
  55. 134 @span.resource = verb
  56. # Add additional request specific tags to the span.
  57. 134 @span.set_tag(TAG_URL, @request.path)
  58. 134 @span.set_tag(TAG_METHOD, verb)
  59. 134 @span.set_tag(TAG_TARGET_HOST, uri.host)
  60. 134 @span.set_tag(TAG_TARGET_PORT, uri.port.to_s)
  61. # Tag as an external peer service
  62. 134 @span.set_tag(TAG_PEER_SERVICE, @span.service)
  63. 134 propagate_headers if @configuration[:distributed_tracing]
  64. # Set analytics sample rate
  65. 134 if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
  66. 16 Contrib::Analytics.set_sample_rate(@span, @configuration[:analytics_sample_rate])
  67. end
  68. rescue StandardError => e
  69. Datadog.logger.error("error preparing span for http request: #{e}")
  70. Datadog.logger.error(e.backtrace)
  71. end
  72. 8 def finish(response)
  73. 134 return unless @span
  74. 134 if response.is_a?(::HTTPX::ErrorResponse)
  75. 12 @span.set_error(response.error)
  76. else
  77. 122 @span.set_tag(TAG_STATUS_CODE, response.status.to_s)
  78. 122 @span.set_error(::HTTPX::HTTPError.new(response)) if response.status >= 400 && response.status <= 599
  79. end
  80. 134 @span.finish
  81. end
  82. 8 private
  83. 8 if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
  84. 8 def build_span
  85. 134 TRACING_MODULE.trace(
  86. SPAN_REQUEST,
  87. service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
  88. span_type: TYPE_OUTBOUND
  89. )
  90. end
  91. 8 def propagate_headers
  92. 126 TRACING_MODULE::Propagation::HTTP.inject!(TRACING_MODULE.active_trace, @request.headers)
  93. end
  94. 8 def configuration
  95. 134 @configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
  96. end
  97. 8 def tracing_enabled?
  98. 134 TRACING_MODULE.enabled?
  99. end
  100. else
  101. def build_span
  102. service_name = configuration[:split_by_domain] ? @request.uri.host : configuration[:service_name]
  103. configuration[:tracer].trace(
  104. SPAN_REQUEST,
  105. service: service_name,
  106. span_type: TYPE_OUTBOUND
  107. )
  108. end
  109. def propagate_headers
  110. Datadog::HTTPPropagator.inject!(@span.context, @request.headers)
  111. end
  112. def configuration
  113. @configuration ||= Datadog.configuration[:httpx, @request.uri.host]
  114. end
  115. def tracing_enabled?
  116. configuration[:tracer].enabled
  117. end
  118. end
  119. end
  120. 8 module RequestMethods
  121. 8 def __datadog_enable_trace!
  122. 134 return super if @__datadog_enable_trace
  123. 134 RequestTracer.new(self).call
  124. 134 @__datadog_enable_trace = true
  125. end
  126. end
  127. 8 module ConnectionMethods
  128. 8 def send(request)
  129. 134 request.__datadog_enable_trace!
  130. 134 super
  131. end
  132. end
  133. end
  134. 8 module Configuration
  135. # Default settings for httpx
  136. #
  137. 8 class Settings < TRACING_MODULE::Contrib::Configuration::Settings
  138. 8 DEFAULT_ERROR_HANDLER = lambda do |response|
  139. Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
  140. end
  141. 8 option :service_name, default: "httpx"
  142. 8 option :distributed_tracing, default: true
  143. 8 option :split_by_domain, default: false
  144. 8 option :enabled do |o|
  145. 104 o.default { env_to_bool("DD_TRACE_HTTPX_ENABLED", true) }
  146. 8 o.lazy
  147. end
  148. 8 option :analytics_enabled do |o|
  149. 93 o.default { env_to_bool(%w[DD_TRACE_HTTPX_ANALYTICS_ENABLED DD_HTTPX_ANALYTICS_ENABLED], false) }
  150. 8 o.lazy
  151. end
  152. 8 option :analytics_sample_rate do |o|
  153. 16 o.default { env_to_float(%w[DD_TRACE_HTTPX_ANALYTICS_SAMPLE_RATE DD_HTTPX_ANALYTICS_SAMPLE_RATE], 1.0) }
  154. 8 o.lazy
  155. end
  156. 8 option :error_handler, default: DEFAULT_ERROR_HANDLER
  157. end
  158. end
  159. # Patcher enables patching of 'httpx' with datadog components.
  160. #
  161. 8 module Patcher
  162. 8 include TRACING_MODULE::Contrib::Patcher
  163. 8 module_function
  164. 8 def target_version
  165. 16 Integration.version
  166. end
  167. # loads a session instannce with the datadog plugin, and replaces the
  168. # base HTTPX::Session with the patched session class.
  169. 8 def patch
  170. 8 datadog_session = ::HTTPX.plugin(Plugin)
  171. 8 ::HTTPX.send(:remove_const, :Session)
  172. 8 ::HTTPX.send(:const_set, :Session, datadog_session.class)
  173. end
  174. end
  175. # Datadog Integration for HTTPX.
  176. #
  177. 8 class Integration
  178. 8 include Contrib::Integration
  179. # MINIMUM_VERSION = Gem::Version.new('0.11.0')
  180. 8 MINIMUM_VERSION = Gem::Version.new("0.10.2")
  181. 8 register_as :httpx
  182. 8 def self.version
  183. 304 Gem.loaded_specs["httpx"] && Gem.loaded_specs["httpx"].version
  184. end
  185. 8 def self.loaded?
  186. 96 defined?(::HTTPX::Request)
  187. end
  188. 8 def self.compatible?
  189. 96 super && version >= MINIMUM_VERSION
  190. end
  191. 8 if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
  192. 8 def new_configuration
  193. 101 Configuration::Settings.new
  194. end
  195. else
  196. def default_configuration
  197. Configuration::Settings.new
  198. end
  199. end
  200. 8 def patcher
  201. 192 Patcher
  202. end
  203. end
  204. end
  205. end
  206. end

lib/httpx/adapters/faraday.rb

95.04% lines covered

141 relevant lines. 134 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 10 require "delegate"
  3. 10 require "httpx"
  4. 10 require "faraday"
  5. 10 module Faraday
  6. 10 class Adapter
  7. 10 class HTTPX < Faraday::Adapter
  8. skipped # :nocov:
  9. skipped SSL_ERROR = if defined?(Faraday::SSLError)
  10. skipped Faraday::SSLError
  11. skipped else
  12. skipped Faraday::Error::SSLError
  13. skipped end
  14. skipped
  15. skipped CONNECTION_FAILED_ERROR = if defined?(Faraday::ConnectionFailed)
  16. skipped Faraday::ConnectionFailed
  17. skipped else
  18. skipped Faraday::Error::ConnectionFailed
  19. skipped end
  20. skipped # :nocov:
  21. 10 unless Faraday::RequestOptions.method_defined?(:stream_response?)
  22. module RequestOptionsExtensions
  23. refine Faraday::RequestOptions do
  24. def stream_response?
  25. false
  26. end
  27. end
  28. end
  29. using RequestOptionsExtensions
  30. end
  31. 10 module RequestMixin
  32. 10 using ::HTTPX::HashExtensions
  33. 10 private
  34. 10 def build_request(env)
  35. 190 meth = env[:method]
  36. 63 request_options = {
  37. 126 headers: env.request_headers,
  38. 20 body: env.body,
  39. }
  40. 190 [meth.to_s.upcase, env.url, request_options]
  41. end
  42. 10 def options_from_env(env)
  43. 60 timeout_options = {
  44. 120 connect_timeout: env.request.open_timeout,
  45. 19 operation_timeout: env.request.timeout,
  46. 19 }.compact
  47. 80 options = {
  48. 101 ssl: {},
  49. timeout: timeout_options,
  50. }
  51. 181 options[:ssl][:verify_mode] = OpenSSL::SSL::VERIFY_PEER if env.ssl.verify
  52. 181 options[:ssl][:ca_file] = env.ssl.ca_file if env.ssl.ca_file
  53. 181 options[:ssl][:ca_path] = env.ssl.ca_path if env.ssl.ca_path
  54. 181 options[:ssl][:cert_store] = env.ssl.cert_store if env.ssl.cert_store
  55. 181 options[:ssl][:cert] = env.ssl.client_cert if env.ssl.client_cert
  56. 181 options[:ssl][:key] = env.ssl.client_key if env.ssl.client_key
  57. 181 options[:ssl][:ssl_version] = env.ssl.version if env.ssl.version
  58. 181 options[:ssl][:verify_depth] = env.ssl.verify_depth if env.ssl.verify_depth
  59. 181 options[:ssl][:min_version] = env.ssl.min_version if env.ssl.min_version
  60. 181 options[:ssl][:max_version] = env.ssl.max_version if env.ssl.max_version
  61. 181 options
  62. end
  63. end
  64. 10 include RequestMixin
  65. 10 module OnDataPlugin
  66. 10 module RequestMethods
  67. 10 attr_writer :response_on_data
  68. 10 def response=(response)
  69. 9 super
  70. 9 return if response.is_a?(::HTTPX::ErrorResponse)
  71. 9 response.body.on_data = @response_on_data
  72. end
  73. end
  74. 10 module ResponseBodyMethods
  75. 10 attr_writer :on_data
  76. 10 def write(chunk)
  77. 30 return super unless @on_data
  78. 30 @on_data.call(chunk, chunk.bytesize)
  79. end
  80. end
  81. end
  82. 10 module ReasonPlugin
  83. 10 if RUBY_VERSION < "2.5"
  84. 2 def self.load_dependencies(*)
  85. 2 require "webrick"
  86. end
  87. else
  88. 8 def self.load_dependencies(*)
  89. 14 require "net/http/status"
  90. end
  91. end
  92. 10 module ResponseMethods
  93. 10 if RUBY_VERSION < "2.5"
  94. 2 def reason
  95. 36 WEBrick::HTTPStatus::StatusMessage.fetch(@status)
  96. end
  97. else
  98. 8 def reason
  99. 127 Net::HTTP::STATUS_CODES.fetch(@status)
  100. end
  101. end
  102. end
  103. end
  104. 10 def self.session
  105. 181 @session ||= ::HTTPX.plugin(:compression).plugin(:persistent).plugin(ReasonPlugin)
  106. end
  107. 10 class ParallelManager
  108. 10 class ResponseHandler < SimpleDelegator
  109. 10 attr_reader :env
  110. 10 def initialize(env)
  111. 27 @env = env
  112. 27 super
  113. end
  114. 10 def on_response(&blk)
  115. 54 if blk
  116. 27 @on_response = ->(response) do
  117. 27 blk.call(response)
  118. end
  119. 27 self
  120. else
  121. 27 @on_response
  122. end
  123. end
  124. 10 def on_complete(&blk)
  125. 54 if blk
  126. 27 @on_complete = blk
  127. 27 self
  128. else
  129. 27 @on_complete
  130. end
  131. end
  132. end
  133. 10 include RequestMixin
  134. 10 def initialize
  135. 27 @handlers = []
  136. end
  137. 10 def enqueue(request)
  138. 27 handler = ResponseHandler.new(request)
  139. 27 @handlers << handler
  140. 27 handler
  141. end
  142. 10 def run
  143. 27 return unless @handlers.last
  144. 18 env = @handlers.last.env
  145. 18 session = HTTPX.session.with(options_from_env(env))
  146. 18 session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
  147. 18 session = session.plugin(OnDataPlugin) if env.request.stream_response?
  148. 45 requests = @handlers.map { |handler| session.build_request(*build_request(handler.env)) }
  149. 18 if env.request.stream_response?
  150. requests.each do |request|
  151. request.response_on_data = env.request.on_data
  152. end
  153. end
  154. 18 responses = session.request(*requests)
  155. 18 Array(responses).each_with_index do |response, index|
  156. 27 handler = @handlers[index]
  157. 27 handler.on_response.call(response)
  158. 27 handler.on_complete.call(handler.env)
  159. end
  160. end
  161. end
  162. 10 self.supports_parallel = true
  163. 10 class << self
  164. 10 def setup_parallel_manager
  165. 27 ParallelManager.new
  166. end
  167. end
  168. 10 def initialize(app, options = {})
  169. 181 super(app)
  170. 181 @session_options = options
  171. end
  172. 10 def call(env)
  173. 190 super
  174. 190 if parallel?(env)
  175. 27 handler = env[:parallel_manager].enqueue(env)
  176. 27 handler.on_response do |response|
  177. 27 if response.is_a?(::HTTPX::Response)
  178. 18 save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
  179. 18 response_headers.merge!(response.headers)
  180. end
  181. else
  182. 9 env[:error] = response.error
  183. 9 save_response(env, 0, "", {}, nil)
  184. end
  185. end
  186. 27 return handler
  187. end
  188. 163 session = HTTPX.session
  189. 163 session = session.with(@session_options) unless @session_options.empty?
  190. 163 session = session.with(options_from_env(env))
  191. 163 session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
  192. 163 session = session.plugin(OnDataPlugin) if env.request.stream_response?
  193. 163 request = session.build_request(*build_request(env))
  194. 163 request.response_on_data = env.request.on_data if env.request.stream_response?
  195. 163 response = session.request(request)
  196. # do not call #raise_for_status for HTTP 4xx or 5xx, as faraday has a middleware for that.
  197. 163 response.raise_for_status unless response.is_a?(::HTTPX::Response)
  198. 145 save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
  199. 145 response_headers.merge!(response.headers)
  200. end
  201. 145 @app.call(env)
  202. rescue ::HTTPX::TLSError => e
  203. 9 raise SSL_ERROR, e
  204. rescue Errno::ECONNABORTED,
  205. Errno::ECONNREFUSED,
  206. Errno::ECONNRESET,
  207. Errno::EHOSTUNREACH,
  208. Errno::EINVAL,
  209. Errno::ENETUNREACH,
  210. Errno::EPIPE,
  211. ::HTTPX::ConnectionError => e
  212. 9 raise CONNECTION_FAILED_ERROR, e
  213. end
  214. 10 private
  215. 10 def parallel?(env)
  216. 190 env[:parallel_manager]
  217. end
  218. end
  219. 10 register_middleware httpx: HTTPX
  220. end
  221. end

lib/httpx/adapters/sentry.rb

100.0% lines covered

60 relevant lines. 60 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 7 require "sentry-ruby"
  3. 7 module HTTPX::Plugins
  4. 7 module Sentry
  5. 7 module Tracer
  6. 7 module_function
  7. 7 def call(request)
  8. 87 sentry_span = start_sentry_span
  9. 87 return unless sentry_span
  10. 87 set_sentry_trace_header(request, sentry_span)
  11. 87 request.on(:response, &method(:finish_sentry_span).curry(3)[sentry_span, request])
  12. end
  13. 7 def start_sentry_span
  14. 87 return unless ::Sentry.initialized? && (span = ::Sentry.get_current_scope.get_span)
  15. 87 return if span.sampled == false
  16. 87 span.start_child(op: "httpx.client", start_timestamp: ::Sentry.utc_now.to_f)
  17. end
  18. 7 def set_sentry_trace_header(request, sentry_span)
  19. 87 return unless sentry_span
  20. 87 trace = ::Sentry.get_current_client.generate_sentry_trace(sentry_span)
  21. 87 request.headers[::Sentry::SENTRY_TRACE_HEADER_NAME] = trace if trace
  22. end
  23. 7 def finish_sentry_span(span, request, response)
  24. 87 return unless ::Sentry.initialized?
  25. 87 record_sentry_breadcrumb(request, response)
  26. 87 record_sentry_span(request, response, span)
  27. end
  28. 7 def record_sentry_breadcrumb(req, res)
  29. 87 return unless ::Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
  30. 87 request_info = extract_request_info(req)
  31. 87 data = if res.is_a?(HTTPX::ErrorResponse)
  32. 10 { error: res.error.message, **request_info }
  33. else
  34. 77 { status: res.status, **request_info }
  35. end
  36. 87 crumb = ::Sentry::Breadcrumb.new(
  37. level: :info,
  38. category: "httpx",
  39. type: :info,
  40. data: data
  41. )
  42. 87 ::Sentry.add_breadcrumb(crumb)
  43. end
  44. 7 def record_sentry_span(req, res, sentry_span)
  45. 87 return unless sentry_span
  46. 87 request_info = extract_request_info(req)
  47. 87 sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
  48. 87 if res.is_a?(HTTPX::ErrorResponse)
  49. 10 sentry_span.set_data(:error, res.error.message)
  50. else
  51. 77 sentry_span.set_data(:status, res.status)
  52. end
  53. 87 sentry_span.set_timestamp(::Sentry.utc_now.to_f)
  54. end
  55. 7 def extract_request_info(req)
  56. 174 uri = req.uri
  57. 54 result = {
  58. 120 method: req.verb,
  59. }
  60. 174 if ::Sentry.configuration.send_default_pii
  61. 28 uri += "?#{req.query}" unless req.query.empty?
  62. 28 result[:body] = req.body.to_s unless req.body.empty? || req.body.unbounded_body?
  63. end
  64. 174 result[:url] = uri.to_s
  65. 174 result
  66. end
  67. end
  68. 7 module RequestMethods
  69. 7 def __sentry_enable_trace!
  70. 87 return super if @__sentry_enable_trace
  71. 87 Tracer.call(self)
  72. 87 @__sentry_enable_trace = true
  73. end
  74. end
  75. 7 module ConnectionMethods
  76. 7 def send(request)
  77. 87 request.__sentry_enable_trace!
  78. 87 super
  79. end
  80. end
  81. end
  82. end
  83. 7 Sentry.register_patch do
  84. 35 sentry_session = HTTPX.plugin(HTTPX::Plugins::Sentry)
  85. 35 HTTPX.send(:remove_const, :Session)
  86. 35 HTTPX.send(:const_set, :Session, sentry_session.class)
  87. end

lib/httpx/adapters/webmock.rb

100.0% lines covered

76 relevant lines. 76 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 9 module WebMock
  3. 9 module HttpLibAdapters
  4. 9 if RUBY_VERSION < "2.5"
  5. 2 require "webrick/httpstatus"
  6. 2 HTTP_REASONS = WEBrick::HTTPStatus::StatusMessage
  7. else
  8. 7 require "net/http/status"
  9. 7 HTTP_REASONS = Net::HTTP::STATUS_CODES
  10. end
  11. #
  12. # HTTPX plugin for webmock.
  13. #
  14. # Requests are "hijacked" at the session, before they're distributed to a connection.
  15. #
  16. 9 module Plugin
  17. 9 class << self
  18. 9 def build_webmock_request_signature(request)
  19. 161 uri = WebMock::Util::URI.heuristic_parse(request.uri)
  20. 161 uri.query = request.query
  21. 161 uri.path = uri.normalized_path.gsub("[^:]//", "/")
  22. 161 WebMock::RequestSignature.new(
  23. request.verb.downcase.to_sym,
  24. uri.to_s,
  25. body: request.body.each.to_a.join,
  26. headers: request.headers.to_h
  27. )
  28. end
  29. 9 def build_webmock_response(_request, response)
  30. 8 webmock_response = WebMock::Response.new
  31. 8 webmock_response.status = [response.status, HTTP_REASONS[response.status]]
  32. 8 webmock_response.body = response.body.to_s
  33. 8 webmock_response.headers = response.headers.to_h
  34. 8 webmock_response
  35. end
  36. 9 def build_from_webmock_response(request, webmock_response)
  37. 145 return build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
  38. 121 return build_error_response(request, webmock_response.exception) if webmock_response.exception
  39. 113 response = request.options.response_class.new(request,
  40. webmock_response.status[0],
  41. "2.0",
  42. webmock_response.headers)
  43. 113 response << webmock_response.body.dup
  44. 113 response
  45. end
  46. 9 def build_error_response(request, exception)
  47. 32 HTTPX::ErrorResponse.new(request, exception, request.options)
  48. end
  49. end
  50. 9 module InstanceMethods
  51. 9 def build_connection(*)
  52. 129 connection = super
  53. 129 connection.once(:unmock_connection) do
  54. 8 pool.__send__(:resolve_connection, connection)
  55. 8 pool.__send__(:unregister_connection, connection) unless connection.addresses
  56. end
  57. 129 connection
  58. end
  59. end
  60. 9 module ConnectionMethods
  61. 9 def initialize(*)
  62. 129 super
  63. 129 @mocked = true
  64. end
  65. 9 def open?
  66. 137 return true if @mocked
  67. 8 super
  68. end
  69. 9 def interests
  70. 3296 return if @mocked
  71. 55 super
  72. end
  73. 9 def send(request)
  74. 161 request_signature = Plugin.build_webmock_request_signature(request)
  75. 161 WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
  76. 161 if (mock_response = WebMock::StubRegistry.instance.response_for_request(request_signature))
  77. 145 response = Plugin.build_from_webmock_response(request, mock_response)
  78. 145 WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
  79. 145 log { "mocking #{request.uri} with #{mock_response.inspect}" }
  80. 145 request.response = response
  81. 145 request.emit(:response, response)
  82. 16 elsif WebMock.net_connect_allowed?(request_signature.uri)
  83. 8 if WebMock::CallbackRegistry.any_callbacks?
  84. 8 request.on(:response) do |resp|
  85. 8 unless resp.is_a?(HTTPX::ErrorResponse)
  86. 8 webmock_response = Plugin.build_webmock_response(request, resp)
  87. 8 WebMock::CallbackRegistry.invoke_callbacks(
  88. { lib: :httpx, real_request: true }, request_signature,
  89. webmock_response
  90. )
  91. end
  92. end
  93. end
  94. 8 @mocked = false
  95. 8 emit(:unmock_connection, self)
  96. 8 super
  97. else
  98. 8 raise WebMock::NetConnectNotAllowedError, request_signature
  99. end
  100. end
  101. end
  102. end
  103. 9 class HttpxAdapter < HttpLibAdapter
  104. 9 adapter_for :httpx
  105. 9 class << self
  106. 9 def enable!
  107. 330 @original_session ||= HTTPX::Session
  108. 330 webmock_session = HTTPX.plugin(Plugin)
  109. 330 HTTPX.send(:remove_const, :Session)
  110. 330 HTTPX.send(:const_set, :Session, webmock_session.class)
  111. end
  112. 9 def disable!
  113. 330 return unless @original_session
  114. 321 HTTPX.send(:remove_const, :Session)
  115. 321 HTTPX.send(:const_set, :Session, @original_session)
  116. end
  117. end
  118. end
  119. end
  120. end

lib/httpx/altsvc.rb

98.18% lines covered

55 relevant lines. 54 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "strscan"
  3. 26 module HTTPX
  4. 26 module AltSvc
  5. 26 @altsvc_mutex = Mutex.new
  6. 53 @altsvcs = Hash.new { |h, k| h[k] = [] }
  7. 26 module_function
  8. 26 def cached_altsvc(origin)
  9. 2675 now = Utils.now
  10. 2675 @altsvc_mutex.synchronize do
  11. 2675 lookup(origin, now)
  12. end
  13. end
  14. 26 def cached_altsvc_set(origin, entry)
  15. 27 now = Utils.now
  16. 27 @altsvc_mutex.synchronize do
  17. 27 return if @altsvcs[origin].any? { |altsvc| altsvc["origin"] == entry["origin"] }
  18. 27 entry["TTL"] = Integer(entry["ma"]) + now if entry.key?("ma")
  19. 27 @altsvcs[origin] << entry
  20. 27 entry
  21. end
  22. end
  23. 26 def lookup(origin, ttl)
  24. 2675 return [] unless @altsvcs.key?(origin)
  25. 36 @altsvcs[origin] = @altsvcs[origin].select do |entry|
  26. 27 !entry.key?("TTL") || entry["TTL"] > ttl
  27. end
  28. 54 @altsvcs[origin].reject { |entry| entry["noop"] }
  29. end
  30. 26 def emit(request, response)
  31. 6955 return unless response.respond_to?(:headers)
  32. # Alt-Svc
  33. 6939 return unless response.headers.key?("alt-svc")
  34. 84 origin = request.origin
  35. 84 host = request.uri.host
  36. 84 altsvc = response.headers["alt-svc"]
  37. # https://tools.ietf.org/html/rfc7838#section-3
  38. # A field value containing the special value "clear" indicates that the
  39. # origin requests all alternatives for that origin to be invalidated
  40. # (including those specified in the same response, in case of an
  41. # invalid reply containing both "clear" and alternative services).
  42. 84 if altsvc == "clear"
  43. 9 @altsvc_mutex.synchronize do
  44. 9 @altsvcs[origin].clear
  45. end
  46. 9 return
  47. end
  48. 75 parse(altsvc) do |alt_origin, alt_params|
  49. 9 alt_origin.host ||= host
  50. 9 yield(alt_origin, origin, alt_params)
  51. end
  52. end
  53. 26 def parse(altsvc)
  54. 201 return enum_for(__method__, altsvc) unless block_given?
  55. 138 scanner = StringScanner.new(altsvc)
  56. 138 until scanner.eos?
  57. 138 alt_service = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
  58. 138 alt_params = []
  59. 138 loop do
  60. 165 alt_param = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
  61. 165 alt_params << alt_param.strip if alt_param
  62. 165 scanner.skip(/;/)
  63. 165 break if scanner.eos? || scanner.scan(/ *, */)
  64. end
  65. 276 alt_params = Hash[alt_params.map { |field| field.split("=") }]
  66. 138 alt_proto, alt_authority = alt_service.split("=")
  67. 138 alt_origin = parse_altsvc_origin(alt_proto, alt_authority)
  68. 138 return unless alt_origin
  69. 54 yield(alt_origin, alt_params.merge("proto" => alt_proto))
  70. end
  71. end
  72. 26 def parse_altsvc_scheme(alt_proto)
  73. 138 case alt_proto
  74. when "h2c"
  75. "http"
  76. when "h2"
  77. 54 "https"
  78. end
  79. end
  80. skipped # :nocov:
  81. skipped if RUBY_VERSION < "2.2"
  82. skipped def parse_altsvc_origin(alt_proto, alt_origin)
  83. skipped alt_scheme = parse_altsvc_scheme(alt_proto) or return
  84. skipped
  85. skipped alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
  86. skipped if alt_origin.start_with?(":")
  87. skipped alt_origin = "#{alt_scheme}://dummy#{alt_origin}"
  88. skipped uri = URI.parse(alt_origin)
  89. skipped uri.host = nil
  90. skipped uri
  91. skipped else
  92. skipped URI.parse("#{alt_scheme}://#{alt_origin}")
  93. skipped end
  94. skipped end
  95. skipped else
  96. skipped def parse_altsvc_origin(alt_proto, alt_origin)
  97. skipped alt_scheme = parse_altsvc_scheme(alt_proto) or return
  98. skipped alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
  99. skipped
  100. skipped URI.parse("#{alt_scheme}://#{alt_origin}")
  101. skipped end
  102. skipped end
  103. skipped # :nocov:
  104. end
  105. end

lib/httpx/buffer.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "forwardable"
  3. 26 module HTTPX
  4. 26 class Buffer
  5. 26 extend Forwardable
  6. 26 def_delegator :@buffer, :<<
  7. 26 def_delegator :@buffer, :to_s
  8. 26 def_delegator :@buffer, :to_str
  9. 26 def_delegator :@buffer, :empty?
  10. 26 def_delegator :@buffer, :bytesize
  11. 26 def_delegator :@buffer, :clear
  12. 26 def_delegator :@buffer, :replace
  13. 26 attr_reader :limit
  14. 26 def initialize(limit)
  15. 13674 @buffer = "".b
  16. 13674 @limit = limit
  17. end
  18. 26 def full?
  19. 3010229 @buffer.bytesize >= @limit
  20. end
  21. 26 def capacity
  22. 5 @limit - @buffer.bytesize
  23. end
  24. 26 def shift!(fin)
  25. 20090 @buffer = @buffer.byteslice(fin..-1) || "".b
  26. end
  27. end
  28. end

lib/httpx/callbacks.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 module Callbacks
  4. 26 def on(type, &action)
  5. 151692 callbacks(type) << action
  6. end
  7. 26 def once(type, &block)
  8. 576 on(type) do |*args, &callback|
  9. 250 block.call(*args, &callback)
  10. 178 :delete
  11. end
  12. end
  13. 26 def only(type, &block)
  14. 20223 callbacks(type).clear
  15. 20223 on(type, &block)
  16. end
  17. 26 def emit(type, *args)
  18. 158731 callbacks(type).delete_if { |pr| :delete == pr.call(*args) } # rubocop:disable Style/YodaCondition
  19. end
  20. 26 def callbacks_for?(type)
  21. 3116 @callbacks.key?(type) && @callbacks[type].any?
  22. end
  23. 26 protected
  24. 26 def callbacks(type = nil)
  25. 263359 return @callbacks unless type
  26. 437473 @callbacks ||= Hash.new { |h, k| h[k] = [] }
  27. 263260 @callbacks[type]
  28. end
  29. end
  30. end

lib/httpx/chainable.rb

90.63% lines covered

32 relevant lines. 29 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 module Chainable
  4. 26 %w[head get post put delete trace options connect patch].each do |meth|
  5. 234 class_eval(<<-MOD, __FILE__, __LINE__ + 1)
  6. def #{meth}(*uri, **options) # def get(*uri, **options)
  7. request("#{meth.upcase}", uri, **options) # request("GET", uri, **options)
  8. end # end
  9. MOD
  10. end
  11. 26 def request(*args, **options)
  12. 1344 branch(default_options).request(*args, **options)
  13. end
  14. skipped # :nocov:
  15. skipped def timeout(**args)
  16. skipped warn ":#{__method__} is deprecated, use :with_timeout instead"
  17. skipped with(timeout: args)
  18. skipped end
  19. skipped
  20. skipped def headers(headers)
  21. skipped warn ":#{__method__} is deprecated, use :with_headers instead"
  22. skipped with(headers: headers)
  23. skipped end
  24. skipped # :nocov:
  25. 26 def accept(type)
  26. 18 with(headers: { "accept" => String(type) })
  27. end
  28. 26 def wrap(&blk)
  29. 109 branch(default_options).wrap(&blk)
  30. end
  31. 26 def plugin(pl, options = nil, &blk)
  32. 4833 klass = is_a?(Session) ? self.class : Session
  33. 4833 klass = Class.new(klass)
  34. 4833 klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
  35. 4833 klass.plugin(pl, options, &blk).new
  36. end
  37. # deprecated
  38. skipped # :nocov:
  39. skipped def plugins(pls)
  40. skipped warn ":#{__method__} is deprecated, use :plugin instead"
  41. skipped klass = is_a?(Session) ? self.class : Session
  42. skipped klass = Class.new(klass)
  43. skipped klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
  44. skipped klass.plugins(pls).new
  45. skipped end
  46. skipped # :nocov:
  47. 26 def with(options, &blk)
  48. 2378 branch(default_options.merge(options), &blk)
  49. end
  50. 26 private
  51. 26 def default_options
  52. 8682 @options || Session.default_options
  53. end
  54. 26 def branch(options, &blk)
  55. 3831 return self.class.new(options, &blk) if is_a?(Session)
  56. 1786 Session.new(options, &blk)
  57. end
  58. 26 def method_missing(meth, *args, **options)
  59. 860 return super unless meth =~ /\Awith_(.+)/
  60. 860 option = Regexp.last_match(1)
  61. 860 return super unless option
  62. 860 with(option.to_sym => (args.first || options))
  63. end
  64. 26 def respond_to_missing?(meth, *)
  65. return super unless meth =~ /\Awith_(.+)/
  66. option = Regexp.last_match(1)
  67. default_options.respond_to?(option) || super
  68. end
  69. end
  70. end

lib/httpx/connection.rb

95.59% lines covered

363 relevant lines. 347 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "resolv"
  3. 26 require "forwardable"
  4. 26 require "httpx/io"
  5. 26 require "httpx/buffer"
  6. 26 module HTTPX
  7. # The Connection can be watched for IO events.
  8. #
  9. # It contains the +io+ object to read/write from, and knows what to do when it can.
  10. #
  11. # It defers connecting until absolutely necessary. Connection should be triggered from
  12. # the IO selector (until then, any request will be queued).
  13. #
  14. # A connection boots up its parser after connection is established. All pending requests
  15. # will be redirected there after connection.
  16. #
  17. # A connection can be prevented from closing by the parser, that is, if there are pending
  18. # requests. This will signal that the connection was prematurely closed, due to a possible
  19. # number of conditions:
  20. #
  21. # * Remote peer closed the connection ("Connection: close");
  22. # * Remote peer doesn't support pipelining;
  23. #
  24. # A connection may also route requests for a different host for which the +io+ was connected
  25. # to, provided that the IP is the same and the port and scheme as well. This will allow to
  26. # share the same socket to send HTTP/2 requests to different hosts.
  27. #
  28. 26 class Connection
  29. 26 extend Forwardable
  30. 26 include Loggable
  31. 26 include Callbacks
  32. 26 using URIExtensions
  33. 26 using NumericExtensions
  34. 26 require "httpx/connection/http2"
  35. 26 require "httpx/connection/http1"
  36. 26 def_delegator :@io, :closed?
  37. 26 def_delegator :@write_buffer, :empty?
  38. 26 attr_reader :type, :io, :origin, :origins, :state, :pending, :options
  39. 26 attr_writer :timers
  40. 26 attr_accessor :family
  41. 26 def initialize(type, uri, options)
  42. 6568 @type = type
  43. 6568 @origins = [uri.origin]
  44. 6568 @origin = Utils.to_uri(uri.origin)
  45. 6568 @options = Options.new(options)
  46. 6568 @window_size = @options.window_size
  47. 6568 @read_buffer = Buffer.new(@options.buffer_size)
  48. 6568 @write_buffer = Buffer.new(@options.buffer_size)
  49. 6568 @pending = []
  50. 6568 on(:error, &method(:on_error))
  51. 6568 if @options.io
  52. # if there's an already open IO, get its
  53. # peer address, and force-initiate the parser
  54. 68 transition(:already_open)
  55. 68 @io = build_socket
  56. 68 parser
  57. else
  58. 6500 transition(:idle)
  59. end
  60. 6568 @inflight = 0
  61. 6568 @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
  62. 6568 @total_timeout = @options.timeout[:total_timeout]
  63. 6568 self.addresses = @options.addresses if @options.addresses
  64. end
  65. # this is a semi-private method, to be used by the resolver
  66. # to initiate the io object.
  67. 26 def addresses=(addrs)
  68. 6227 if @io
  69. @io.add_addresses(addrs)
  70. else
  71. 6227 @io = build_socket(addrs)
  72. end
  73. end
  74. 26 def addresses
  75. 20714 @io && @io.addresses
  76. end
  77. 26 def match?(uri, options)
  78. 5015 return false if @state == :closing || @state == :closed
  79. 4857 return false if exhausted?
  80. (
  81. (
  82. 3974 @origins.include?(uri.origin) &&
  83. # if there is more than one origin to match, it means that this connection
  84. # was the result of coalescing. To prevent blind trust in the case where the
  85. # origin came from an ORIGIN frame, we're going to verify the hostname with the
  86. # SSL certificate
  87. 2779 (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
  88. 146 ) && @options == options
  89. 5021 ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
  90. end
  91. 26 def mergeable?(connection)
  92. 3230 return false if @state == :closing || @state == :closed || !@io
  93. 935 return false if exhausted?
  94. 926 return false unless connection.addresses
  95. (
  96. 926 (open? && @origin == connection.origin) ||
  97. 878 !(@io.addresses & (connection.addresses || [])).empty?
  98. 338 ) && @options == connection.options
  99. end
  100. # coalescable connections need to be mergeable!
  101. # but internally, #mergeable? is called before #coalescable?
  102. 26 def coalescable?(connection)
  103. 10 if @io.protocol == "h2" &&
  104. 2 @origin.scheme == "https" &&
  105. connection.origin.scheme == "https" &&
  106. @io.can_verify_peer?
  107. 9 @io.verify_hostname(connection.origin.host)
  108. else
  109. 3 @origin == connection.origin
  110. end
  111. end
  112. 26 def create_idle(options = {})
  113. 27 self.class.new(@type, @origin, @options.merge(options))
  114. end
  115. 26 def merge(connection)
  116. 45 @origins |= connection.instance_variable_get(:@origins)
  117. 45 connection.purge_pending do |req|
  118. 27 send(req)
  119. end
  120. end
  121. 26 def purge_pending(&block)
  122. 54 pendings = []
  123. 54 if @parser
  124. 45 @inflight -= @parser.pending.size
  125. 45 pendings << @parser.pending
  126. end
  127. 54 pendings << @pending
  128. 54 pendings.each do |pending|
  129. 99 pending.reject!(&block)
  130. end
  131. end
  132. # checks if this is connection is an alternative service of
  133. # +uri+
  134. 26 def match_altsvcs?(uri)
  135. 7285 @origins.any? { |origin| uri.altsvc_match?(origin) } ||
  136. 58 AltSvc.cached_altsvc(@origin).any? do |altsvc|
  137. origin = altsvc["origin"]
  138. origin.altsvc_match?(uri.origin)
  139. 838 end
  140. end
  141. 26 def match_altsvc_options?(uri, options)
  142. 946 return @options == options unless @options.ssl[:hostname] == uri.host
  143. 9 dup_options = @options.merge(ssl: { hostname: nil })
  144. 9 dup_options.ssl.delete(:hostname)
  145. 9 dup_options == options
  146. end
  147. 26 def connecting?
  148. 3240930 @state == :idle
  149. end
  150. 26 def inflight?
  151. 6015 @parser && !@parser.empty? && !@write_buffer.empty?
  152. end
  153. 26 def interests
  154. # connecting
  155. 3227155 if connecting?
  156. 13220 connect
  157. 13220 return @io.interests if connecting?
  158. end
  159. # if the write buffer is full, we drain it
  160. 3220271 return :w unless @write_buffer.empty?
  161. 3176656 return @parser.interests if @parser
  162. 5 nil
  163. end
  164. 26 def to_io
  165. 24769 @io.to_io
  166. end
  167. 26 def call
  168. 21347 case @state
  169. when :closed
  170. return
  171. when :closing
  172. 2538 consume
  173. 2538 transition(:closed)
  174. 2538 emit(:close)
  175. when :open
  176. 11952 consume
  177. end
  178. 1858 nil
  179. end
  180. 26 def close
  181. 6050 transition(:active) if @state == :inactive
  182. 6050 @parser.close if @parser
  183. end
  184. # bypasses the state machine to force closing of connections still connecting.
  185. # **only** used for Happy Eyeballs v2.
  186. 26 def force_reset
  187. @state = :closing
  188. transition(:closed)
  189. emit(:close)
  190. end
  191. 26 def reset
  192. 3509 transition(:closing)
  193. 3509 transition(:closed)
  194. 3509 emit(:close)
  195. end
  196. 26 def send(request)
  197. 7359 if @parser && !@write_buffer.full?
  198. 476 request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
  199. 476 if @response_received_at && @keep_alive_timeout &&
  200. 36 Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
  201. # when pushing a request into an existing connection, we have to check whether there
  202. # is the possibility that the connection might have extended the keep alive timeout.
  203. # for such cases, we want to ping for availability before deciding to shovel requests.
  204. 9 log(level: 3) { "keep alive timeout expired, pinging connection..." }
  205. 9 @pending << request
  206. 9 parser.ping
  207. 9 transition(:active) if @state == :inactive
  208. 9 return
  209. end
  210. 467 send_request_to_parser(request)
  211. else
  212. 6883 @pending << request
  213. end
  214. end
  215. 26 def timeout
  216. 4181482 if @total_timeout
  217. 1235 return @total_timeout unless @connected_at
  218. 473 elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
  219. 473 if elapsed_time.negative?
  220. 1 ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
  221. 1 ex.set_backtrace(caller)
  222. 1 on_error(ex)
  223. 1 return
  224. end
  225. 472 return elapsed_time
  226. end
  227. 4180247 return @timeout if defined?(@timeout)
  228. 68 return @options.timeout[:connect_timeout] if @state == :idle
  229. 68 @options.timeout[:operation_timeout]
  230. end
  231. 26 def deactivate
  232. 1033 transition(:inactive)
  233. end
  234. 26 def open?
  235. 7293 @state == :open || @state == :inactive
  236. end
  237. 26 def raise_timeout_error(interval)
  238. 362 error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
  239. 362 error.set_backtrace(caller)
  240. 362 on_error(error)
  241. end
  242. 26 private
  243. 26 def connect
  244. 12399 transition(:open)
  245. end
  246. 26 def exhausted?
  247. 5792 @parser && parser.exhausted?
  248. end
  249. 26 def consume
  250. 14760 return unless @io
  251. 14760 catch(:called) do
  252. 14760 epiped = false
  253. 14760 loop do
  254. 33654 parser.consume
  255. # we exit if there's no more requests to process
  256. #
  257. # this condition takes into account:
  258. #
  259. # * the number of inflight requests
  260. # * the number of pending requests
  261. # * whether the write buffer has bytes (i.e. for close handshake)
  262. 33654 if @pending.empty? && @inflight.zero? && @write_buffer.empty?
  263. 2850 log(level: 3) { "NO MORE REQUESTS..." }
  264. 2841 return
  265. end
  266. 30813 @timeout = @current_timeout
  267. 30813 read_drained = false
  268. 30813 write_drained = nil
  269. #
  270. # tight read loop.
  271. #
  272. # read as much of the socket as possible.
  273. #
  274. # this tight loop reads all the data it can from the socket and pipes it to
  275. # its parser.
  276. #
  277. 1573 loop do
  278. 50057 siz = @io.read(@window_size, @read_buffer)
  279. 50139 log(level: 3, color: :cyan) { "IO READ: #{siz} bytes..." }
  280. 50057 unless siz
  281. 9 ex = EOFError.new("descriptor closed")
  282. 9 ex.set_backtrace(caller)
  283. 9 on_error(ex)
  284. 9 return
  285. end
  286. # socket has been drained. mark and exit the read loop.
  287. 50048 if siz.zero?
  288. 5127 read_drained = @read_buffer.empty?
  289. 5127 epiped = false
  290. 5127 break
  291. end
  292. 44921 parser << @read_buffer.to_s
  293. # continue reading if possible.
  294. 41352 break if interests == :w && !epiped
  295. # exit the read loop if connection is preparing to be closed
  296. 34268 break if @state == :closing || @state == :closed
  297. # exit #consume altogether if all outstanding requests have been dealt with
  298. 34260 return if @pending.empty? && @inflight.zero?
  299. 30813 end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
  300. #
  301. # tight write loop.
  302. #
  303. # flush as many bytes as the sockets allow.
  304. #
  305. 1906 loop do
  306. # buffer has been drainned, mark and exit the write loop.
  307. 23081 if @write_buffer.empty?
  308. # we only mark as drained on the first loop
  309. 3286 write_drained = write_drained.nil? && @inflight.positive?
  310. 3286 break
  311. end
  312. 8254 begin
  313. 19795 siz = @io.write(@write_buffer)
  314. rescue Errno::EPIPE
  315. # this can happen if we still have bytes in the buffer to send to the server, but
  316. # the server wants to respond immediately with some message, or an error. An example is
  317. # when one's uploading a big file to an unintended endpoint, and the server stops the
  318. # consumption, and responds immediately with an authorization of even method not allowed error.
  319. # at this point, we have to let the connection switch to read-mode.
  320. 19 log(level: 2) { "pipe broken, could not flush buffer..." }
  321. 19 epiped = true
  322. 19 read_drained = false
  323. 19 break
  324. end
  325. 19839 log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
  326. 19776 unless siz
  327. ex = EOFError.new("descriptor closed")
  328. ex.set_backtrace(caller)
  329. on_error(ex)
  330. return
  331. end
  332. # socket closed for writing. mark and exit the write loop.
  333. 19776 if siz.zero?
  334. 12 write_drained = !@write_buffer.empty?
  335. 12 break
  336. end
  337. # exit write loop if marked to consume from peer, or is closing.
  338. 19764 break if interests == :r || @state == :closing || @state == :closed
  339. 3617 write_drained = false
  340. 24582 end unless (ints = interests) == :r
  341. 24582 send_pending if @state == :open
  342. # return if socket is drained
  343. 24582 next unless (ints != :r || read_drained) && (ints != :w || write_drained)
  344. # gotta go back to the event loop. It happens when:
  345. #
  346. # * the socket is drained of bytes or it's not the interest of the conn to read;
  347. # * theres nothing more to write, or it's not in the interest of the conn to write;
  348. 5696 log(level: 3) { "(#{ints}): WAITING FOR EVENTS..." }
  349. 5688 return
  350. end
  351. end
  352. end
  353. 26 def send_pending
  354. 59672 while !@write_buffer.full? && (request = @pending.shift)
  355. 6868 send_request_to_parser(request)
  356. end
  357. end
  358. 26 def parser
  359. 95306 @parser ||= build_parser
  360. end
  361. 26 def send_request_to_parser(request)
  362. 7333 @inflight += 1
  363. 7333 parser.send(request)
  364. 7333 set_request_timeouts(request)
  365. 7333 return unless @state == :inactive
  366. 184 transition(:active)
  367. end
  368. 647 def build_parser(protocol = @io.protocol)
  369. 6022 parser = self.class.parser_type(protocol).new(@write_buffer, @options)
  370. 6022 set_parser_callbacks(parser)
  371. 6022 parser
  372. end
  373. 26 def set_parser_callbacks(parser)
  374. 6147 parser.on(:response) do |request, response|
  375. 6946 AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
  376. 9 emit(:altsvc, alt_origin, origin, alt_params)
  377. end
  378. 6946 @response_received_at = Utils.now
  379. 6946 @inflight -= 1
  380. 6946 request.emit(:response, response)
  381. end
  382. 6147 parser.on(:altsvc) do |alt_origin, origin, alt_params|
  383. emit(:altsvc, alt_origin, origin, alt_params)
  384. end
  385. 6147 parser.on(:pong, &method(:send_pending))
  386. 6147 parser.on(:promise) do |request, stream|
  387. 27 request.emit(:promise, parser, stream)
  388. end
  389. 6147 parser.on(:exhausted) do
  390. 18 emit(:exhausted)
  391. end
  392. 6147 parser.on(:origin) do |origin|
  393. @origins |= [origin]
  394. end
  395. 6147 parser.on(:close) do |force|
  396. 5871 transition(:closing)
  397. 5871 if force || @state == :idle
  398. 3131 transition(:closed)
  399. 3131 emit(:close)
  400. end
  401. end
  402. 6147 parser.on(:close_handshake) do
  403. 9 consume
  404. end
  405. 6147 parser.on(:reset) do
  406. 3324 if parser.empty?
  407. 3134 reset
  408. else
  409. 190 transition(:closing)
  410. 190 transition(:closed)
  411. 190 emit(:reset)
  412. 190 @parser.reset if @parser
  413. 190 transition(:idle)
  414. 190 transition(:open)
  415. end
  416. end
  417. 6147 parser.on(:current_timeout) do
  418. 2745 @current_timeout = @timeout = parser.timeout
  419. end
  420. 6147 parser.on(:timeout) do |tout|
  421. @timeout = tout
  422. end
  423. 6147 parser.on(:error) do |request, ex|
  424. 369 case ex
  425. when MisdirectedRequestError
  426. 9 emit(:misdirected, request)
  427. else
  428. 360 response = ErrorResponse.new(request, ex, @options)
  429. 360 request.response = response
  430. 360 request.emit(:response, response)
  431. end
  432. end
  433. end
  434. 26 def transition(nextstate)
  435. 41429 handle_transition(nextstate)
  436. rescue Errno::ECONNABORTED,
  437. Errno::ECONNREFUSED,
  438. Errno::ECONNRESET,
  439. Errno::EADDRNOTAVAIL,
  440. Errno::EHOSTUNREACH,
  441. Errno::EINVAL,
  442. Errno::ENETUNREACH,
  443. Errno::EPIPE,
  444. Errno::ENOENT,
  445. SocketError => e
  446. # connect errors, exit gracefully
  447. 66 error = ConnectionError.new(e.message)
  448. 66 error.set_backtrace(e.backtrace)
  449. 66 connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
  450. 66 @state = :closed
  451. 66 emit(:close)
  452. rescue TLSError => e
  453. # connect errors, exit gracefully
  454. 27 handle_error(e)
  455. 27 connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
  456. 27 @state = :closed
  457. 27 emit(:close)
  458. end
  459. 26 def handle_transition(nextstate)
  460. 41057 case nextstate
  461. when :idle
  462. 6707 @timeout = @current_timeout = @options.timeout[:connect_timeout]
  463. when :open
  464. 12819 return if @state == :closed
  465. 12819 @io.connect
  466. 12726 emit(:tcp_open, self) if @io.state == :connected
  467. 12726 return unless @io.connected?
  468. 6251 @connected_at = Utils.now
  469. 6251 send_pending
  470. 6251 @timeout = @current_timeout = parser.timeout
  471. 6251 emit(:open)
  472. when :inactive
  473. 1033 return unless @state == :open
  474. when :closing
  475. 9834 return unless @state == :open
  476. when :closed
  477. 9584 return unless @state == :closing
  478. 6384 return unless @write_buffer.empty?
  479. 6339 purge_after_closed
  480. when :already_open
  481. 68 nextstate = :open
  482. 68 send_pending
  483. when :active
  484. 475 return unless @state == :inactive
  485. 475 nextstate = :open
  486. 475 emit(:activate)
  487. end
  488. 27290 @state = nextstate
  489. end
  490. 26 def purge_after_closed
  491. 6347 @io.close if @io
  492. 6347 @read_buffer.clear
  493. 6347 remove_instance_variable(:@timeout) if defined?(@timeout)
  494. end
  495. 26 def build_socket(addrs = nil)
  496. 6295 transport_type = case @type
  497. 3597 when "tcp" then TCP
  498. 2666 when "ssl" then SSL
  499. 32 when "unix" then UNIX
  500. else
  501. raise Error, "unsupported transport (#{@type})"
  502. end
  503. 6295 transport_type.new(@origin, addrs, @options)
  504. end
  505. 26 def on_error(error)
  506. 628 if error.instance_of?(TimeoutError)
  507. 362 if @total_timeout && @connected_at &&
  508. 30 Utils.elapsed_time(@connected_at) > @total_timeout
  509. 280 ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
  510. 280 ex.set_backtrace(error.backtrace)
  511. 280 error = ex
  512. else
  513. # inactive connections do not contribute to the select loop, therefore
  514. # they should not fail due to such errors.
  515. 82 return if @state == :inactive
  516. 82 if @timeout
  517. 82 @timeout -= error.timeout
  518. 82 return unless @timeout <= 0
  519. end
  520. 18 error = error.to_connection_error if connecting?
  521. end
  522. end
  523. 564 handle_error(error)
  524. 564 reset
  525. end
  526. 26 def handle_error(error)
  527. 684 parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
  528. 1585 while (request = @pending.shift)
  529. 298 response = ErrorResponse.new(request, error, request.options)
  530. 298 request.response = response
  531. 298 request.emit(:response, response)
  532. end
  533. end
  534. 26 def set_request_timeouts(request)
  535. 7333 write_timeout = request.write_timeout
  536. 1 request.once(:headers) do
  537. 36 @timers.after(write_timeout) { write_timeout_callback(request, write_timeout) }
  538. 7333 end unless write_timeout.nil? || write_timeout.infinite?
  539. 7333 read_timeout = request.read_timeout
  540. 1 request.once(:done) do
  541. 27 @timers.after(read_timeout) { read_timeout_callback(request, read_timeout) }
  542. 7333 end unless read_timeout.nil? || read_timeout.infinite?
  543. 7333 request_timeout = request.request_timeout
  544. 17 request.once(:headers) do
  545. 27 @timers.after(request_timeout) { read_timeout_callback(request, request_timeout, RequestTimeoutError) }
  546. 7333 end unless request_timeout.nil? || request_timeout.infinite?
  547. end
  548. 26 def write_timeout_callback(request, write_timeout)
  549. 18 return if request.state == :done
  550. 9 @write_buffer.clear
  551. 9 error = WriteTimeoutError.new(request, nil, write_timeout)
  552. 9 on_error(error)
  553. end
  554. 26 def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
  555. 18 response = request.response
  556. 18 return if response && response.finished?
  557. 18 @write_buffer.clear
  558. 18 error = error_type.new(request, request.response, read_timeout)
  559. 18 on_error(error)
  560. end
  561. 26 class << self
  562. 26 def parser_type(protocol)
  563. 6169 case protocol
  564. 2752 when "h2" then HTTP2
  565. 3417 when "http/1.1" then HTTP1
  566. else
  567. raise Error, "unsupported protocol (##{protocol})"
  568. end
  569. end
  570. end
  571. end
  572. end

lib/httpx/connection/http1.rb

92.45% lines covered

212 relevant lines. 196 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "httpx/parser/http1"
  3. 26 module HTTPX
  4. 26 class Connection::HTTP1
  5. 26 include Callbacks
  6. 26 include Loggable
  7. 26 MAX_REQUESTS = 100
  8. 26 CRLF = "\r\n"
  9. 26 attr_reader :pending, :requests
  10. 26 def initialize(buffer, options)
  11. 3417 @options = Options.new(options)
  12. 3417 @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
  13. 3417 @max_requests = @options.max_requests || MAX_REQUESTS
  14. 3417 @parser = Parser::HTTP1.new(self)
  15. 3417 @buffer = buffer
  16. 3417 @version = [1, 1]
  17. 3417 @pending = []
  18. 3417 @requests = []
  19. 3417 @handshake_completed = false
  20. end
  21. 26 def timeout
  22. 3517 @options.timeout[:operation_timeout]
  23. end
  24. 26 def interests
  25. # this means we're processing incoming response already
  26. 206916 return :r if @request
  27. 200904 return if @requests.empty?
  28. 19915 request = @requests.first
  29. 19915 return unless request
  30. 19915 return :w if request.interests == :w || !@buffer.empty?
  31. 13196 :r
  32. end
  33. 26 def reset
  34. 3362 @max_requests = @options.max_requests || MAX_REQUESTS
  35. 3362 @parser.reset!
  36. 3362 @handshake_completed = false
  37. end
  38. 26 def close
  39. 3137 reset
  40. 3137 emit(:close, true)
  41. end
  42. 26 def exhausted?
  43. 563 !@max_requests.positive?
  44. end
  45. 26 def empty?
  46. # this means that for every request there's an available
  47. # partial response, so there are no in-flight requests waiting.
  48. 5028 @requests.empty? || (
  49. # checking all responses can be time-consuming. Alas, as in HTTP/1, responses
  50. # do not come out of order, we can get away with checking first and last.
  51. 296 !@requests.first.response.nil? &&
  52. 175 (@requests.size == 1 || !@requests.last.response.nil?)
  53. 1412 )
  54. end
  55. 26 def <<(data)
  56. 6892 @parser << data
  57. end
  58. 26 def send(request)
  59. 4119 unless @max_requests.positive?
  60. @pending << request
  61. return
  62. end
  63. 4119 return if @requests.include?(request)
  64. 4119 @requests << request
  65. 4119 @pipelining = true if @requests.size > 1
  66. end
  67. 26 def consume
  68. 12570 requests_limit = [@max_requests, @requests.size].min
  69. 12570 concurrent_requests_limit = [@max_concurrent_requests, requests_limit].min
  70. 12570 @requests.each_with_index do |request, idx|
  71. 14794 break if idx >= concurrent_requests_limit
  72. 12639 next if request.state == :done
  73. 5907 handle(request)
  74. end
  75. end
  76. # HTTP Parser callbacks
  77. #
  78. # must be public methods, or else they won't be reachable
  79. 26 def on_start
  80. 4065 log(level: 2) { "parsing begins" }
  81. end
  82. 26 def on_headers(h)
  83. 4038 @request = @requests.first
  84. 4038 return if @request.response
  85. 4065 log(level: 2) { "headers received" }
  86. 4038 headers = @request.options.headers_class.new(h)
  87. 4399 response = @request.options.response_class.new(@request,
  88. 360 @parser.status_code,
  89. 360 @parser.http_version.join("."),
  90. headers)
  91. 4065 log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
  92. 4281 log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
  93. 4038 @request.response = response
  94. 4038 on_complete if response.finished?
  95. end
  96. 26 def on_trailers(h)
  97. 9 return unless @request
  98. 9 response = @request.response
  99. 9 log(level: 2) { "trailer headers received" }
  100. 9 log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v.join(", ")}" }.join("\n") }
  101. 9 response.merge_headers(h)
  102. end
  103. 26 def on_data(chunk)
  104. 5168 return unless @request
  105. 5195 log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
  106. 5195 log(level: 2, color: :green) { "-> #{chunk.inspect}" }
  107. 5168 response = @request.response
  108. 5168 response << chunk
  109. end
  110. 26 def on_complete
  111. 4029 return unless @request
  112. 4056 log(level: 2) { "parsing complete" }
  113. 4029 dispatch
  114. end
  115. 26 def dispatch
  116. 4029 if @request.expects?
  117. 81 @parser.reset!
  118. 81 return handle(@request)
  119. end
  120. 3948 request = @request
  121. 3948 @request = nil
  122. 3948 @requests.shift
  123. 3948 response = request.response
  124. 3948 response.finish!
  125. 3948 emit(:response, request, response)
  126. 3909 if @parser.upgrade?
  127. 35 response << @parser.upgrade_data
  128. 35 throw(:called)
  129. end
  130. 3874 @parser.reset!
  131. 3874 @max_requests -= 1
  132. 3874 manage_connection(response)
  133. 532 send(@pending.shift) unless @pending.empty?
  134. end
  135. 26 def handle_error(ex)
  136. 162 if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request && @request.response &&
  137. !@request.response.headers.key?("content-length") &&
  138. !@request.response.headers.key?("transfer-encoding")
  139. # if the response does not contain a content-length header, the server closing the
  140. # connnection is the indicator of response consumed.
  141. # https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.4
  142. 18 catch(:called) { on_complete }
  143. 9 return
  144. end
  145. 153 if @pipelining
  146. catch(:called) { disable }
  147. else
  148. 153 @requests.each do |request|
  149. 153 emit(:error, request, ex)
  150. end
  151. 153 @pending.each do |request|
  152. emit(:error, request, ex)
  153. end
  154. end
  155. end
  156. 26 def ping
  157. emit(:reset)
  158. emit(:exhausted)
  159. end
  160. 26 private
  161. 26 def manage_connection(response)
  162. 3874 connection = response.headers["connection"]
  163. 3874 case connection
  164. when /keep-alive/i
  165. 532 if @handshake_completed
  166. if @max_requests.zero?
  167. @pending.concat(@requests)
  168. @requests.clear
  169. emit(:exhausted)
  170. end
  171. return
  172. end
  173. 532 keep_alive = response.headers["keep-alive"]
  174. 532 return unless keep_alive
  175. 118 parameters = Hash[keep_alive.split(/ *, */).map do |pair|
  176. 118 pair.split(/ *= */)
  177. end]
  178. 118 @max_requests = parameters["max"].to_i - 1 if parameters.key?("max")
  179. 118 if parameters.key?("timeout")
  180. keep_alive_timeout = parameters["timeout"].to_i
  181. emit(:timeout, keep_alive_timeout)
  182. end
  183. 118 @handshake_completed = true
  184. when /close/i
  185. 3342 disable
  186. when nil
  187. # In HTTP/1.1, it's keep alive by default
  188. return if response.version == "1.1"
  189. disable
  190. end
  191. end
  192. 26 def disable
  193. 3342 disable_pipelining
  194. 3342 emit(:reset)
  195. 3342 throw(:called)
  196. end
  197. 26 def disable_pipelining
  198. 3342 return if @requests.empty?
  199. # do not disable pipelining if already set to 1 request at a time
  200. 199 return if @max_concurrent_requests == 1
  201. 34 @requests.each do |r|
  202. 34 r.transition(:idle)
  203. # when we disable pipelining, we still want to try keep-alive.
  204. # only when keep-alive with one request fails, do we fallback to
  205. # connection: close.
  206. 34 r.headers["connection"] = "close" if @max_concurrent_requests == 1
  207. end
  208. # server doesn't handle pipelining, and probably
  209. # doesn't support keep-alive. Fallback to send only
  210. # 1 keep alive request.
  211. 34 @max_concurrent_requests = 1
  212. 34 @pipelining = false
  213. end
  214. 26 def set_protocol_headers(request)
  215. 3341 if !request.headers.key?("content-length") &&
  216. 1004 request.body.bytesize == Float::INFINITY
  217. 36 request.body.chunk!
  218. end
  219. 4126 connection = request.headers["connection"]
  220. 4120 connection ||= if request.options.persistent
  221. # when in a persistent connection, the request can't be at
  222. # the edge of a renegotiation
  223. 838 if @requests.index(request) + 1 < @max_requests
  224. 707 "keep-alive"
  225. else
  226. 131 "close"
  227. end
  228. else
  229. # when it's not a persistent connection, it sets "Connection: close" always
  230. # on the last request of the possible batch (either allowed max requests,
  231. # or if smaller, the size of the batch itself)
  232. 3261 requests_limit = [@max_requests, @requests.size].min
  233. 3261 if request == @requests[requests_limit - 1]
  234. 3208 "close"
  235. else
  236. 53 "keep-alive"
  237. end
  238. 785 end
  239. 4126 extra_headers = { "connection" => connection }
  240. 4126 extra_headers["host"] = request.authority unless request.headers.key?("host")
  241. 4126 extra_headers
  242. end
  243. 26 def handle(request)
  244. 5988 catch(:buffer_full) do
  245. 5988 request.transition(:headers)
  246. 5988 join_headers(request) if request.state == :headers
  247. 5988 request.transition(:body)
  248. 5988 join_body(request) if request.state == :body
  249. 4314 request.transition(:trailers)
  250. # HTTP/1.1 trailers should only work for chunked encoding
  251. 4314 join_trailers(request) if request.body.chunked? && request.state == :trailers
  252. 4314 request.transition(:done)
  253. end
  254. end
  255. 26 def join_headline(request)
  256. 4054 "#{request.verb} #{request.path} HTTP/#{@version.join(".")}"
  257. end
  258. 26 def join_headers(request)
  259. 4126 headline = join_headline(request)
  260. 4126 @buffer << headline << CRLF
  261. 4153 log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
  262. 4126 extra_headers = set_protocol_headers(request)
  263. 4126 join_headers2(request.headers.each(extra_headers))
  264. 4153 log { "<- " }
  265. 4126 @buffer << CRLF
  266. end
  267. 26 def join_body(request)
  268. 5791 return if request.body.empty?
  269. 7746 while (chunk = request.drain_body)
  270. 3961 log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
  271. 3961 log(level: 2, color: :green) { "<- #{chunk.inspect}" }
  272. 3961 @buffer << chunk
  273. 3961 throw(:buffer_full, request) if @buffer.full?
  274. end
  275. 1456 return unless (error = request.drain_error)
  276. raise error
  277. end
  278. 26 def join_trailers(request)
  279. 108 return unless request.trailers? && request.callbacks_for?(:trailers)
  280. 36 join_headers2(request.trailers)
  281. 36 log { "<- " }
  282. 36 @buffer << CRLF
  283. end
  284. 26 def join_headers2(headers)
  285. 4162 buffer = "".b
  286. 4162 headers.each do |field, value|
  287. 21509 buffer << "#{capitalized(field)}: #{value}" << CRLF
  288. 21617 log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
  289. 21509 @buffer << buffer
  290. 21509 buffer.clear
  291. end
  292. end
  293. 23 UPCASED = {
  294. 2 "www-authenticate" => "WWW-Authenticate",
  295. "http2-settings" => "HTTP2-Settings",
  296. }.freeze
  297. 26 def capitalized(field)
  298. 21509 UPCASED[field] || field.split("-").map(&:capitalize).join("-")
  299. end
  300. end
  301. end

lib/httpx/connection/http2.rb

96.11% lines covered

257 relevant lines. 247 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "securerandom"
  3. 26 require "http/2/next"
  4. 26 module HTTPX
  5. 26 class Connection::HTTP2
  6. 26 include Callbacks
  7. 26 include Loggable
  8. 26 MAX_CONCURRENT_REQUESTS = HTTP2Next::DEFAULT_MAX_CONCURRENT_STREAMS
  9. 26 class Error < Error
  10. 26 def initialize(id, code)
  11. 42 super("stream #{id} closed with error: #{code}")
  12. end
  13. end
  14. 26 class GoawayError < Error
  15. 26 def initialize
  16. 18 super(0, :no_error)
  17. end
  18. end
  19. 26 attr_reader :streams, :pending
  20. 26 def initialize(buffer, options)
  21. 2778 @options = Options.new(options)
  22. 2778 @settings = @options.http2_settings
  23. 2778 @pending = []
  24. 2778 @streams = {}
  25. 2778 @drains = {}
  26. 2778 @pings = []
  27. 2778 @buffer = buffer
  28. 2778 @handshake_completed = false
  29. 2778 @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
  30. 2778 @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
  31. 2778 @max_requests = @options.max_requests || 0
  32. 2778 init_connection
  33. end
  34. 26 def timeout
  35. 5479 return @options.timeout[:operation_timeout] if @handshake_completed
  36. 2734 @options.timeout[:settings_timeout]
  37. end
  38. 26 def interests
  39. # waiting for WINDOW_UPDATE frames
  40. 2969647 return :r if @buffer.full?
  41. 2969647 if @connection.state == :closed
  42. 2839 return unless @handshake_completed
  43. 2780 return :w
  44. end
  45. 2966808 unless (@connection.state == :connected && @handshake_completed)
  46. 11382 return @buffer.empty? ? :r : :rw
  47. end
  48. 2955426 return :w if !@pending.empty? && can_buffer_more_requests?
  49. 2955426 return :w unless @drains.empty?
  50. 2954489 if @buffer.empty?
  51. 2954489 return if @streams.empty? && @pings.empty?
  52. 42594 return :r
  53. end
  54. :rw
  55. end
  56. 26 def close
  57. 2756 @connection.goaway unless @connection.state == :closed
  58. 2756 emit(:close)
  59. end
  60. 26 def empty?
  61. 2517 @connection.state == :closed || @streams.empty?
  62. end
  63. 26 def exhausted?
  64. 4692 return false if @max_requests.zero? && @connection.active_stream_count.zero?
  65. 4656 @connection.active_stream_count >= @max_requests
  66. end
  67. 26 def <<(data)
  68. 37777 @connection << data
  69. end
  70. 26 def can_buffer_more_requests?
  71. 6531 if @handshake_completed
  72. 2852 @streams.size < @max_concurrent_requests &&
  73. 1106 @streams.size < @max_requests
  74. else
  75. 2272 !@wait_for_handshake &&
  76. 624 @streams.size < @max_concurrent_requests
  77. end
  78. end
  79. 26 def send(request)
  80. 6055 unless can_buffer_more_requests?
  81. 2819 @pending << request
  82. 2819 return
  83. end
  84. 3236 unless (stream = @streams[request])
  85. 3236 stream = @connection.new_stream
  86. 3227 handle_stream(stream, request)
  87. 3227 @streams[request] = stream
  88. 3227 @max_requests -= 1
  89. end
  90. 3227 handle(request, stream)
  91. 3227 true
  92. rescue HTTP2Next::Error::StreamLimitExceeded
  93. 9 @pending.unshift(request)
  94. 9 emit(:exhausted)
  95. end
  96. 26 def consume
  97. 20674 @streams.each do |request, stream|
  98. 7663 next if request.state == :done
  99. 1064 handle(request, stream)
  100. end
  101. end
  102. 26 def handle_error(ex)
  103. 208 if ex.instance_of?(TimeoutError) && !@handshake_completed && @connection.state != :closed
  104. 9 @connection.goaway(:settings_timeout, "closing due to settings timeout")
  105. 9 emit(:close_handshake)
  106. 9 settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
  107. 9 settings_ex.set_backtrace(ex.backtrace)
  108. 9 ex = settings_ex
  109. end
  110. 208 @streams.each_key do |request|
  111. 172 emit(:error, request, ex)
  112. end
  113. 208 @pending.each do |request|
  114. 35 emit(:error, request, ex)
  115. end
  116. end
  117. 26 def ping
  118. 9 ping = SecureRandom.gen_random(8)
  119. 9 @connection.ping(ping)
  120. ensure
  121. 9 @pings << ping
  122. end
  123. 26 private
  124. 26 def send_pending
  125. 7554 while (request = @pending.shift)
  126. 2708 break unless send(request)
  127. end
  128. end
  129. 26 def handle(request, stream)
  130. 4354 catch(:buffer_full) do
  131. 4354 request.transition(:headers)
  132. 4354 join_headers(stream, request) if request.state == :headers
  133. 4354 request.transition(:body)
  134. 4354 join_body(stream, request) if request.state == :body
  135. 3376 request.transition(:trailers)
  136. 3376 join_trailers(stream, request) if request.state == :trailers && !request.body.empty?
  137. 3376 request.transition(:done)
  138. end
  139. end
  140. 26 def init_connection
  141. 2778 @connection = HTTP2Next::Client.new(@settings)
  142. 2778 @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
  143. 2778 @connection.on(:frame, &method(:on_frame))
  144. 2778 @connection.on(:frame_sent, &method(:on_frame_sent))
  145. 2778 @connection.on(:frame_received, &method(:on_frame_received))
  146. 2778 @connection.on(:origin, &method(:on_origin))
  147. 2778 @connection.on(:promise, &method(:on_promise))
  148. 2778 @connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
  149. 2778 @connection.on(:settings_ack, &method(:on_settings))
  150. 2778 @connection.on(:ack, &method(:on_pong))
  151. 2778 @connection.on(:goaway, &method(:on_close))
  152. #
  153. # Some servers initiate HTTP/2 negotiation right away, some don't.
  154. # As such, we have to check the socket buffer. If there is something
  155. # to read, the server initiated the negotiation. If not, we have to
  156. # initiate it.
  157. #
  158. 2778 @connection.send_connection_preface
  159. end
  160. 26 alias_method :reset, :init_connection
  161. 26 public :reset
  162. 26 def handle_stream(stream, request)
  163. 3245 request.on(:refuse, &method(:on_stream_refuse).curry(3)[stream, request])
  164. 3245 stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
  165. 3245 stream.on(:half_close) do
  166. 3220 log(level: 2) { "#{stream.id}: waiting for response..." }
  167. end
  168. 3245 stream.on(:altsvc, &method(:on_altsvc).curry(2)[request.origin])
  169. 3245 stream.on(:headers, &method(:on_stream_headers).curry(3)[stream, request])
  170. 3245 stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
  171. end
  172. 26 def set_protocol_headers(request)
  173. 352 {
  174. 2177 ":scheme" => request.scheme,
  175. 321 ":method" => request.verb,
  176. 321 ":path" => request.path,
  177. 321 ":authority" => request.authority,
  178. 698 }
  179. end
  180. 26 def join_headers(stream, request)
  181. 3227 extra_headers = set_protocol_headers(request)
  182. 3227 if request.headers.key?("host")
  183. 9 log { "forbidden \"host\" header found (#{request.headers["host"]}), will use it as authority..." }
  184. 9 extra_headers[":authority"] = request.headers["host"]
  185. end
  186. 3227 log(level: 1, color: :yellow) do
  187. 63 request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
  188. end
  189. 3227 stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
  190. end
  191. 26 def join_trailers(stream, request)
  192. 1430 unless request.trailers?
  193. 1421 stream.data("", end_stream: true) if request.callbacks_for?(:trailers)
  194. 1421 return
  195. end
  196. 9 log(level: 1, color: :yellow) do
  197. request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
  198. end
  199. 9 stream.headers(request.trailers.each, end_stream: true)
  200. end
  201. 26 def join_body(stream, request)
  202. 4205 return if request.body.empty?
  203. 2408 chunk = @drains.delete(request) || request.drain_body
  204. 2408 while chunk
  205. 2692 next_chunk = request.drain_body
  206. 2692 log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
  207. 2692 log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
  208. 2692 stream.data(chunk, end_stream: !(next_chunk || request.trailers? || request.callbacks_for?(:trailers)))
  209. 2692 if next_chunk && (@buffer.full? || request.body.unbounded_body?)
  210. 978 @drains[request] = next_chunk
  211. 978 throw(:buffer_full)
  212. end
  213. 1714 chunk = next_chunk
  214. end
  215. 1430 return unless (error = request.drain_error)
  216. 16 on_stream_refuse(stream, request, error)
  217. end
  218. ######
  219. # HTTP/2 Callbacks
  220. ######
  221. 26 def on_stream_headers(stream, request, h)
  222. 3289 response = request.response
  223. 3289 if response.is_a?(Response) && response.version == "2.0"
  224. 152 on_stream_trailers(stream, response, h)
  225. 152 return
  226. end
  227. 3137 log(color: :yellow) do
  228. 81 h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
  229. end
  230. 3137 _, status = h.shift
  231. 3137 headers = request.options.headers_class.new(h)
  232. 3137 response = request.options.response_class.new(request, status, "2.0", headers)
  233. 3137 request.response = response
  234. 3137 @streams[request] = stream
  235. 3137 handle(request, stream) if request.expects?
  236. end
  237. 26 def on_stream_trailers(stream, response, h)
  238. 152 log(color: :yellow) do
  239. h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
  240. end
  241. 152 response.merge_headers(h)
  242. end
  243. 26 def on_stream_data(stream, request, data)
  244. 7052 log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
  245. 7052 log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
  246. 7034 request.response << data
  247. end
  248. 26 def on_stream_refuse(stream, request, error)
  249. 16 on_stream_close(stream, request, error)
  250. 16 stream.close
  251. end
  252. 26 def on_stream_close(stream, request, error)
  253. 3080 return if error == :stream_closed && !@streams.key?(request)
  254. 3073 log(level: 2) { "#{stream.id}: closing stream" }
  255. 3064 @drains.delete(request)
  256. 3064 @streams.delete(request)
  257. 3064 if error
  258. 16 ex = Error.new(stream.id, error)
  259. 16 ex.set_backtrace(caller)
  260. 16 response = ErrorResponse.new(request, ex, request.options)
  261. 16 emit(:response, request, response)
  262. else
  263. 3048 response = request.response
  264. 3048 if response && response.is_a?(Response) && response.status == 421
  265. 9 ex = MisdirectedRequestError.new(response)
  266. 9 ex.set_backtrace(caller)
  267. 9 emit(:error, request, ex)
  268. else
  269. 3039 emit(:response, request, response)
  270. end
  271. end
  272. 3064 send(@pending.shift) unless @pending.empty?
  273. 3064 return unless @streams.empty? && exhausted?
  274. 204 close
  275. 204 emit(:exhausted) unless @pending.empty?
  276. end
  277. 26 def on_frame(bytes)
  278. 18497 @buffer << bytes
  279. end
  280. 26 def on_settings(*)
  281. 2745 @handshake_completed = true
  282. 2745 emit(:current_timeout)
  283. 2745 if @max_requests.zero?
  284. 2557 @max_requests = @connection.remote_settings[:settings_max_concurrent_streams]
  285. 2557 @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
  286. end
  287. 2745 @max_concurrent_requests = [@max_concurrent_requests, @max_requests].min
  288. 2745 send_pending
  289. end
  290. 26 def on_close(_last_frame, error, _payload)
  291. 26 is_connection_closed = @connection.state == :closed
  292. 26 if error
  293. 26 @buffer.clear if is_connection_closed
  294. 26 if error == :no_error
  295. 18 ex = GoawayError.new
  296. 18 @pending.unshift(*@streams.keys)
  297. 18 @drains.clear
  298. 18 @streams.clear
  299. else
  300. 8 ex = Error.new(0, error)
  301. end
  302. 26 ex.set_backtrace(caller)
  303. 26 handle_error(ex)
  304. end
  305. 26 return unless is_connection_closed && @streams.empty?
  306. 26 emit(:close, is_connection_closed)
  307. end
  308. 26 def on_frame_sent(frame)
  309. 15745 log(level: 2) { "#{frame[:stream]}: frame was sent!" }
  310. 15709 log(level: 2, color: :blue) do
  311. 36 payload = frame
  312. 36 payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
  313. 36 "#{frame[:stream]}: #{payload}"
  314. end
  315. end
  316. 26 def on_frame_received(frame)
  317. 17418 log(level: 2) { "#{frame[:stream]}: frame was received!" }
  318. 17373 log(level: 2, color: :magenta) do
  319. 45 payload = frame
  320. 45 payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
  321. 45 "#{frame[:stream]}: #{payload}"
  322. end
  323. end
  324. 26 def on_altsvc(origin, frame)
  325. log(level: 2) { "#{frame[:stream]}: altsvc frame was received" }
  326. log(level: 2) { "#{frame[:stream]}: #{frame.inspect}" }
  327. alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
  328. params = { "ma" => frame[:max_age] }
  329. emit(:altsvc, origin, alt_origin, origin, params)
  330. end
  331. 26 def on_promise(stream)
  332. 27 emit(:promise, @streams.key(stream.parent), stream)
  333. end
  334. 26 def on_origin(origin)
  335. emit(:origin, origin)
  336. end
  337. 26 def on_pong(ping)
  338. 9 if @pings.delete(ping.to_s)
  339. 9 emit(:pong)
  340. else
  341. close(:protocol_error, "ping payload did not match")
  342. end
  343. end
  344. end
  345. end

lib/httpx/domain_name.rb

100.0% lines covered

44 relevant lines. 44 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. #
  3. # domain_name.rb - Domain Name manipulation library for Ruby
  4. #
  5. # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
  6. #
  7. # Redistribution and use in source and binary forms, with or without
  8. # modification, are permitted provided that the following conditions
  9. # are met:
  10. # 1. Redistributions of source code must retain the above copyright
  11. # notice, this list of conditions and the following disclaimer.
  12. # 2. Redistributions in binary form must reproduce the above copyright
  13. # notice, this list of conditions and the following disclaimer in the
  14. # documentation and/or other materials provided with the distribution.
  15. #
  16. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  17. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  18. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  19. # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  20. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  21. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  22. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  23. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  24. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  25. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  26. # SUCH DAMAGE.
  27. 26 require "ipaddr"
  28. 26 module HTTPX
  29. # Represents a domain name ready for extracting its registered domain
  30. # and TLD.
  31. 26 class DomainName
  32. 26 include Comparable
  33. # The full host name normalized, ASCII-ized and downcased using the
  34. # Unicode NFC rules and the Punycode algorithm. If initialized with
  35. # an IP address, the string representation of the IP address
  36. # suitable for opening a connection to.
  37. 26 attr_reader :hostname
  38. # The Unicode representation of the #hostname property.
  39. #
  40. # :attr_reader: hostname_idn
  41. # The least "universally original" domain part of this domain name.
  42. # For example, "example.co.uk" for "www.sub.example.co.uk". This
  43. # may be nil if the hostname does not have one, like when it is an
  44. # IP address, an effective TLD or higher itself, or of a
  45. # non-canonical domain.
  46. 26 attr_reader :domain
  47. 26 DOT = "." # :nodoc:
  48. 26 class << self
  49. 26 def new(domain)
  50. 963 return domain if domain.is_a?(self)
  51. 891 super(domain)
  52. end
  53. # Normalizes a _domain_ using the Punycode algorithm as necessary.
  54. # The result will be a downcased, ASCII-only string.
  55. 26 def normalize(domain)
  56. 855 domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
  57. 855 Punycode.encode_hostname(domain).downcase
  58. end
  59. end
  60. # Parses _hostname_ into a DomainName object. An IP address is also
  61. # accepted. An IPv6 address may be enclosed in square brackets.
  62. 26 def initialize(hostname)
  63. 891 hostname = String(hostname)
  64. 891 raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
  65. 396 begin
  66. 891 @ipaddr = IPAddr.new(hostname)
  67. 36 @hostname = @ipaddr.to_s
  68. 36 return
  69. rescue IPAddr::Error
  70. 855 nil
  71. end
  72. 855 @hostname = DomainName.normalize(hostname)
  73. 855 tld = if (last_dot = @hostname.rindex(DOT))
  74. 207 @hostname[(last_dot + 1)..-1]
  75. else
  76. 648 @hostname
  77. end
  78. # unknown/local TLD
  79. 855 @domain = if last_dot
  80. # fallback - accept cookies down to second level
  81. # cf. http://www.dkim-reputation.org/regdom-libs/
  82. 207 if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
  83. 54 @hostname[(penultimate_dot + 1)..-1]
  84. else
  85. 153 @hostname
  86. end
  87. else
  88. # no domain part - must be a local hostname
  89. 648 tld
  90. end
  91. end
  92. # Checks if the server represented by this domain is qualified to
  93. # send and receive cookies with a domain attribute value of
  94. # _domain_. A true value given as the second argument represents
  95. # cookies without a domain attribute value, in which case only
  96. # hostname equality is checked.
  97. 26 def cookie_domain?(domain, host_only = false)
  98. # RFC 6265 #5.3
  99. # When the user agent "receives a cookie":
  100. 36 return self == @domain if host_only
  101. 36 domain = DomainName.new(domain)
  102. # RFC 6265 #5.1.3
  103. # Do not perform subdomain matching against IP addresses.
  104. 36 @hostname == domain.hostname if @ipaddr
  105. # RFC 6265 #4.1.1
  106. # Domain-value must be a subdomain.
  107. 36 @domain && self <= domain && domain <= @domain
  108. end
  109. # def ==(other)
  110. # other = DomainName.new(other)
  111. # other.hostname == @hostname
  112. # end
  113. 26 def <=>(other)
  114. 54 other = DomainName.new(other)
  115. 54 othername = other.hostname
  116. 54 if othername == @hostname
  117. 18 0
  118. 35 elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
  119. # The other is higher
  120. 18 -1
  121. else
  122. # The other is lower
  123. 18 1
  124. end
  125. end
  126. end
  127. end

lib/httpx/errors.rb

97.62% lines covered

42 relevant lines. 41 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 class Error < StandardError; end
  4. 26 class UnsupportedSchemeError < Error; end
  5. 26 class ConnectionError < Error; end
  6. 26 class TimeoutError < Error
  7. 26 attr_reader :timeout
  8. 26 def initialize(timeout, message)
  9. 725 @timeout = timeout
  10. 725 super(message)
  11. end
  12. 26 def to_connection_error
  13. 9 ex = ConnectTimeoutError.new(@timeout, message)
  14. 9 ex.set_backtrace(backtrace)
  15. 9 ex
  16. end
  17. end
  18. 26 class TotalTimeoutError < TimeoutError; end
  19. 26 class ConnectTimeoutError < TimeoutError; end
  20. 26 class RequestTimeoutError < TimeoutError
  21. 26 attr_reader :request
  22. 26 def initialize(request, response, timeout)
  23. 27 @request = request
  24. 27 @response = response
  25. 27 super(timeout, "Timed out after #{timeout} seconds")
  26. end
  27. 26 def marshal_dump
  28. [message]
  29. end
  30. end
  31. 26 class ReadTimeoutError < RequestTimeoutError; end
  32. 26 class WriteTimeoutError < RequestTimeoutError; end
  33. 26 class SettingsTimeoutError < TimeoutError; end
  34. 26 class ResolveTimeoutError < TimeoutError; end
  35. 26 class ResolveError < Error; end
  36. 26 class NativeResolveError < ResolveError
  37. 26 attr_reader :connection, :host
  38. 26 def initialize(connection, host, message = "Can't resolve #{host}")
  39. 102 @connection = connection
  40. 102 @host = host
  41. 102 super(message)
  42. end
  43. end
  44. 26 class HTTPError < Error
  45. 26 attr_reader :response
  46. 26 def initialize(response)
  47. 88 @response = response
  48. 88 super("HTTP Error: #{@response.status} #{@response.headers}\n#{@response.body}")
  49. end
  50. 26 def status
  51. 18 @response.status
  52. end
  53. end
  54. 26 class MisdirectedRequestError < HTTPError; end
  55. end

lib/httpx/extensions.rb

65.93% lines covered

91 relevant lines. 60 lines covered and 31 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "uri"
  3. 26 module HTTPX
  4. 26 unless Method.method_defined?(:curry)
  5. # Backport
  6. #
  7. # Ruby 2.1 and lower implement curry only for Procs.
  8. #
  9. # Why not using Refinements? Because they don't work for Method (tested with ruby 2.1.9).
  10. #
  11. module CurryMethods
  12. # Backport for the Method#curry method, which is part of ruby core since 2.2 .
  13. #
  14. def curry(*args)
  15. to_proc.curry(*args)
  16. end
  17. end
  18. Method.__send__(:include, CurryMethods)
  19. end
  20. 26 unless String.method_defined?(:+@)
  21. # Backport for +"", to initialize unfrozen strings from the string literal.
  22. #
  23. module LiteralStringExtensions
  24. def +@
  25. frozen? ? dup : self
  26. end
  27. end
  28. String.__send__(:include, LiteralStringExtensions)
  29. end
  30. 26 unless Numeric.method_defined?(:positive?)
  31. # Ruby 2.3 Backport (Numeric#positive?)
  32. #
  33. module PosMethods
  34. def positive?
  35. self > 0
  36. end
  37. end
  38. Numeric.__send__(:include, PosMethods)
  39. end
  40. 26 unless Numeric.method_defined?(:negative?)
  41. # Ruby 2.3 Backport (Numeric#negative?)
  42. #
  43. module NegMethods
  44. def negative?
  45. self < 0
  46. end
  47. end
  48. Numeric.__send__(:include, NegMethods)
  49. end
  50. 26 module NumericExtensions
  51. # Ruby 2.4 backport
  52. 26 refine Numeric do
  53. def infinite?
  54. 6 self == Float::INFINITY
  55. 26 end unless Numeric.method_defined?(:infinite?)
  56. end
  57. end
  58. 26 module StringExtensions
  59. 26 refine String do
  60. # Ruby 2.5 backport
  61. def delete_suffix!(suffix)
  62. suffix = Backports.coerce_to_str(suffix)
  63. chomp! if frozen?
  64. len = suffix.length
  65. if len > 0 && index(suffix, -len)
  66. self[-len..-1] = ''
  67. self
  68. else
  69. nil
  70. end
  71. 26 end unless String.method_defined?(:delete_suffix!)
  72. end
  73. end
  74. 26 module HashExtensions
  75. 26 refine Hash do
  76. # Ruby 2.4 backport
  77. def compact
  78. 21 h = {}
  79. 21 each do |key, value|
  80. 72 h[key] = value unless value == nil
  81. end
  82. 21 h
  83. 26 end unless Hash.method_defined?(:compact)
  84. end
  85. end
  86. 26 module ArrayExtensions
  87. 26 module FilterMap
  88. refine Array do
  89. # Ruby 2.7 backport
  90. 9 def filter_map
  91. 10552202 return to_enum(:filter_map) unless block_given?
  92. 10552202 each_with_object([]) do |item, res|
  93. 10168501 processed = yield(item)
  94. 10168501 res << processed if processed
  95. end
  96. end
  97. 26 end unless Array.method_defined?(:filter_map)
  98. end
  99. 26 module Sum
  100. refine Array do
  101. # Ruby 2.6 backport
  102. 2 def sum(accumulator = 0, &block)
  103. 79 values = block_given? ? map(&block) : self
  104. 79 values.inject(accumulator, :+)
  105. end
  106. 26 end unless Array.method_defined?(:sum)
  107. end
  108. 26 module Intersect
  109. refine Array do
  110. # Ruby 3.1 backport
  111. 13 def intersect?(arr)
  112. if size < arr.size
  113. smaller = self
  114. else
  115. smaller, arr = arr, self
  116. end
  117. (arr & smaller).size > 0
  118. end
  119. 26 end unless Array.method_defined?(:intersect?)
  120. end
  121. end
  122. 26 module IOExtensions
  123. 26 refine IO do
  124. # Ruby 2.3 backport
  125. # provides a fallback for rubies where IO#wait isn't implemented,
  126. # but IO#wait_readable and IO#wait_writable are.
  127. def wait(timeout = nil, _mode = :read_write)
  128. r, w = IO.select([self], [self], nil, timeout)
  129. return unless r || w
  130. self
  131. 26 end unless IO.method_defined?(:wait) && IO.instance_method(:wait).arity == 2
  132. end
  133. end
  134. 26 module RegexpExtensions
  135. 26 refine(Regexp) do
  136. # Ruby 2.4 backport
  137. 26 def match?(*args)
  138. 341 !match(*args).nil?
  139. end
  140. end
  141. end
  142. 26 module URIExtensions
  143. # uri 0.11 backport, ships with ruby 3.1
  144. 26 refine URI::Generic do
  145. 26 def non_ascii_hostname
  146. 373 @non_ascii_hostname
  147. end
  148. 26 def non_ascii_hostname=(hostname)
  149. 36 @non_ascii_hostname = hostname
  150. end
  151. def authority
  152. 27621 return host if port == default_port
  153. 1301 "#{host}:#{port}"
  154. 26 end unless URI::HTTP.method_defined?(:authority)
  155. def origin
  156. 21786 "#{scheme}://#{authority}"
  157. 26 end unless URI::HTTP.method_defined?(:origin)
  158. 26 def altsvc_match?(uri)
  159. 4071 uri = URI.parse(uri)
  160. 3229 origin == uri.origin || begin
  161. 2649 case scheme
  162. when "h2"
  163. (uri.scheme == "https" || uri.scheme == "h2") &&
  164. host == uri.host &&
  165. (port || default_port) == (uri.port || uri.default_port)
  166. else
  167. 2649 false
  168. end
  169. 842 end
  170. end
  171. end
  172. end
  173. end

lib/httpx/headers.rb

100.0% lines covered

72 relevant lines. 72 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 class Headers
  4. 26 class << self
  5. 26 def new(headers = nil)
  6. 42826 return headers if headers.is_a?(self)
  7. 18222 super
  8. end
  9. end
  10. 26 def initialize(headers = nil)
  11. 18222 @headers = {}
  12. 18222 return unless headers
  13. 18037 headers.each do |field, value|
  14. 55790 array_value(value).each do |v|
  15. 55862 add(downcased(field), v)
  16. end
  17. end
  18. end
  19. # cloned initialization
  20. 26 def initialize_clone(orig)
  21. 9 super
  22. 9 @headers = orig.instance_variable_get(:@headers).clone
  23. end
  24. # dupped initialization
  25. 26 def initialize_dup(orig)
  26. 12320 super
  27. 12320 @headers = orig.instance_variable_get(:@headers).dup
  28. end
  29. # freezes the headers hash
  30. 26 def freeze
  31. 30765 @headers.freeze
  32. 30765 super
  33. end
  34. 26 def same_headers?(headers)
  35. 28 @headers.empty? || begin
  36. 36 headers.each do |k, v|
  37. 63 next unless key?(k)
  38. 63 return false unless v == self[k]
  39. end
  40. 18 true
  41. 8 end
  42. end
  43. # merges headers with another header-quack.
  44. # the merge rule is, if the header already exists,
  45. # ignore what the +other+ headers has. Otherwise, set
  46. #
  47. 26 def merge(other)
  48. 4414 headers = dup
  49. 4414 other.each do |field, value|
  50. 4066 headers[downcased(field)] = value
  51. end
  52. 4414 headers
  53. end
  54. # returns the comma-separated values of the header field
  55. # identified by +field+, or nil otherwise.
  56. #
  57. 26 def [](field)
  58. 76098 a = @headers[downcased(field)] || return
  59. 23457 a.join(", ")
  60. end
  61. # sets +value+ (if not nil) as single value for the +field+ header.
  62. #
  63. 26 def []=(field, value)
  64. 28596 return unless value
  65. 28596 @headers[downcased(field)] = array_value(value)
  66. end
  67. # deletes all values associated with +field+ header.
  68. #
  69. 26 def delete(field)
  70. 130 canonical = downcased(field)
  71. 130 @headers.delete(canonical) if @headers.key?(canonical)
  72. end
  73. # adds additional +value+ to the existing, for header +field+.
  74. #
  75. 26 def add(field, value)
  76. 56348 (@headers[downcased(field)] ||= []) << String(value)
  77. end
  78. # helper to be used when adding an header field as a value to another field
  79. #
  80. # h2_headers.add_header("vary", "accept-encoding")
  81. # h2_headers["vary"] #=> "accept-encoding"
  82. # h1_headers.add_header("vary", "accept-encoding")
  83. # h1_headers["vary"] #=> "Accept-Encoding"
  84. #
  85. 26 alias_method :add_header, :add
  86. # returns the enumerable headers store in pairs of header field + the values in
  87. # the comma-separated string format
  88. #
  89. 26 def each(extra_headers = nil)
  90. 34428 return enum_for(__method__, extra_headers) { @headers.size } unless block_given?
  91. 23203 @headers.each do |field, value|
  92. 35217 yield(field, value.join(", ")) unless value.empty?
  93. end
  94. 7352 extra_headers.each do |field, value|
  95. 21235 yield(field, value) unless value.empty?
  96. 23185 end if extra_headers
  97. end
  98. 26 def ==(other)
  99. 3066 other == to_hash
  100. end
  101. # the headers store in Hash format
  102. 26 def to_hash
  103. 3653 Hash[to_a]
  104. end
  105. 26 alias_method :to_h, :to_hash
  106. # the headers store in array of pairs format
  107. 26 def to_a
  108. 3674 Array(each)
  109. end
  110. # headers as string
  111. 26 def to_s
  112. 1371 @headers.to_s
  113. end
  114. skipped # :nocov:
  115. skipped def inspect
  116. skipped to_hash.inspect
  117. skipped end
  118. skipped # :nocov:
  119. # this is internal API and doesn't abide to other public API
  120. # guarantees, like downcasing strings.
  121. # Please do not use this outside of core!
  122. #
  123. 26 def key?(downcased_key)
  124. 31892 @headers.key?(downcased_key)
  125. end
  126. # returns the values for the +field+ header in array format.
  127. # This method is more internal, and for this reason doesn't try
  128. # to "correct" the user input, i.e. it doesn't downcase the key.
  129. #
  130. 26 def get(field)
  131. 660 @headers[field] || EMPTY
  132. end
  133. 26 private
  134. 26 def array_value(value)
  135. 84386 case value
  136. when Array
  137. 61180 value.map { |val| String(val).strip }
  138. else
  139. 54355 [String(value).strip]
  140. end
  141. end
  142. 26 def downcased(field)
  143. 221100 String(field).downcase
  144. end
  145. end
  146. end

lib/httpx/io.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "socket"
  3. 26 require "httpx/io/udp"
  4. 26 require "httpx/io/tcp"
  5. 26 require "httpx/io/unix"
  6. 26 require "httpx/io/ssl"

lib/httpx/io/ssl.rb

93.55% lines covered

62 relevant lines. 58 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "openssl"
  3. 26 module HTTPX
  4. 26 TLSError = OpenSSL::SSL::SSLError
  5. 26 IPRegex = Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
  6. 26 class SSL < TCP
  7. 26 using RegexpExtensions unless Regexp.method_defined?(:match?)
  8. 26 TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
  9. 26 { alpn_protocols: %w[h2 http/1.1].freeze }.freeze
  10. else
  11. {}.freeze
  12. end
  13. 26 def initialize(_, _, options)
  14. 2750 super
  15. 2750 @ctx = OpenSSL::SSL::SSLContext.new
  16. 2750 ctx_options = TLS_OPTIONS.merge(options.ssl)
  17. 2750 @sni_hostname = ctx_options.delete(:hostname) || @hostname
  18. 2750 @ctx.set_params(ctx_options) unless ctx_options.empty?
  19. 2750 @state = :negotiated if @keep_open
  20. 2750 @hostname_is_ip = IPRegex.match?(@sni_hostname)
  21. end
  22. 26 def protocol
  23. 2710 @io.alpn_protocol || super
  24. rescue StandardError
  25. super
  26. end
  27. 26 def can_verify_peer?
  28. 9 @ctx.verify_mode == OpenSSL::SSL::VERIFY_PEER
  29. end
  30. 26 def verify_hostname(host)
  31. 20 return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
  32. 20 return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
  33. 11 OpenSSL::SSL.verify_certificate_identity(@io.peer_cert, host)
  34. end
  35. 26 def close
  36. 2664 super
  37. # allow reconnections
  38. # connect only works if initial @io is a socket
  39. 2664 @io = @io.io if @io.respond_to?(:io)
  40. end
  41. 26 def connected?
  42. 5691 @state == :negotiated
  43. end
  44. 26 def connect
  45. 5734 super
  46. 4507 return if @state == :negotiated ||
  47. 1789 @state != :connected
  48. 3051 unless @io.is_a?(OpenSSL::SSL::SSLSocket)
  49. 2706 @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
  50. 2706 @io.hostname = @sni_hostname unless @hostname_is_ip
  51. 2706 @io.sync_close = true
  52. end
  53. 3051 try_ssl_connect
  54. end
  55. 26 if RUBY_VERSION < "2.3"
  56. skipped # :nocov:
  57. skipped def try_ssl_connect
  58. skipped @io.connect_nonblock
  59. skipped @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
  60. skipped transition(:negotiated)
  61. skipped @interests = :w
  62. skipped rescue ::IO::WaitReadable
  63. skipped @interests = :r
  64. skipped rescue ::IO::WaitWritable
  65. skipped @interests = :w
  66. skipped end
  67. skipped
  68. skipped def read(_, buffer)
  69. skipped super
  70. skipped rescue ::IO::WaitWritable
  71. skipped buffer.clear
  72. skipped 0
  73. skipped end
  74. skipped
  75. skipped def write(*)
  76. skipped super
  77. skipped rescue ::IO::WaitReadable
  78. skipped 0
  79. skipped end
  80. skipped # :nocov:
  81. else
  82. 26 def try_ssl_connect
  83. 3051 case @io.connect_nonblock(exception: false)
  84. when :wait_readable
  85. 353 @interests = :r
  86. 353 return
  87. when :wait_writable
  88. @interests = :w
  89. return
  90. end
  91. 2674 @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
  92. 2671 transition(:negotiated)
  93. 2671 @interests = :w
  94. end
  95. skipped # :nocov:
  96. skipped if OpenSSL::VERSION < "2.0.6"
  97. skipped def read(size, buffer)
  98. skipped @io.read_nonblock(size, buffer)
  99. skipped buffer.bytesize
  100. skipped rescue ::IO::WaitReadable,
  101. skipped ::IO::WaitWritable
  102. skipped buffer.clear
  103. skipped 0
  104. skipped rescue EOFError
  105. skipped nil
  106. skipped end
  107. skipped end
  108. skipped # :nocov:
  109. end
  110. 26 private
  111. 26 def transition(nextstate)
  112. 10570 case nextstate
  113. when :negotiated
  114. 2671 return unless @state == :connected
  115. when :closed
  116. 2077 return unless @state == :negotiated ||
  117. 561 @state == :connected
  118. end
  119. 10570 do_transition(nextstate)
  120. end
  121. 26 def log_transition_state(nextstate)
  122. 44 return super unless nextstate == :negotiated
  123. 9 server_cert = @io.peer_cert
  124. 9 "#{super}\n\n" \
  125. 1 "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
  126. "ALPN, server accepted to use #{protocol}\n" \
  127. "Server certificate:\n " \
  128. "subject: #{server_cert.subject}\n " \
  129. "start date: #{server_cert.not_before}\n " \
  130. "expire date: #{server_cert.not_after}\n " \
  131. "issuer: #{server_cert.issuer}\n " \
  132. "SSL certificate verify ok."
  133. end
  134. end
  135. end

lib/httpx/io/tcp.rb

92.45% lines covered

106 relevant lines. 98 lines covered and 8 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "resolv"
  3. 26 require "ipaddr"
  4. 26 module HTTPX
  5. 26 class TCP
  6. 26 include Loggable
  7. 26 using URIExtensions
  8. 26 attr_reader :ip, :port, :addresses, :state, :interests
  9. 26 alias_method :host, :ip
  10. 26 def initialize(origin, addresses, options)
  11. 6352 @state = :idle
  12. 6352 @addresses = []
  13. 6352 @hostname = origin.host
  14. 6352 @options = Options.new(options)
  15. 6352 @fallback_protocol = @options.fallback_protocol
  16. 6352 @port = origin.port
  17. 6352 @interests = :w
  18. 6352 if @options.io
  19. 52 @io = case @options.io
  20. when Hash
  21. 18 @options.io[origin.authority]
  22. else
  23. 34 @options.io
  24. end
  25. 52 raise Error, "Given IO objects do not match the request authority" unless @io
  26. 52 _, _, _, @ip = @io.addr
  27. 52 @addresses << @ip
  28. 52 @keep_open = true
  29. 52 @state = :connected
  30. else
  31. 6300 add_addresses(addresses)
  32. end
  33. 6352 @ip_index = @addresses.size - 1
  34. # @io ||= build_socket
  35. end
  36. 26 def add_addresses(addrs)
  37. 6300 return if addrs.empty?
  38. 17735 addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
  39. 6300 ip_index = @ip_index || (@addresses.size - 1)
  40. 6300 if addrs.first.ipv6?
  41. # should be the next in line
  42. @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
  43. else
  44. 6300 @addresses.unshift(*addrs)
  45. 6300 @ip_index += addrs.size if @ip_index
  46. end
  47. end
  48. 26 def to_io
  49. 24863 @io.to_io
  50. end
  51. 26 def protocol
  52. 3480 @fallback_protocol
  53. end
  54. 26 def connect
  55. 13413 return unless closed?
  56. 12840 if !@io || @io.closed?
  57. 6406 transition(:idle)
  58. 6406 @io = build_socket
  59. end
  60. 12840 try_connect
  61. rescue Errno::ECONNREFUSED,
  62. Errno::EADDRNOTAVAIL,
  63. Errno::EHOSTUNREACH,
  64. SocketError => e
  65. 66 raise e if @ip_index <= 0
  66. 8 log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
  67. 8 @ip_index -= 1
  68. 8 @io = build_socket
  69. 8 retry
  70. rescue Errno::ETIMEDOUT => e
  71. raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
  72. log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
  73. @ip_index -= 1
  74. @io = build_socket
  75. retry
  76. end
  77. 26 if RUBY_VERSION < "2.3"
  78. skipped # :nocov:
  79. skipped def try_connect
  80. skipped @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
  81. skipped rescue ::IO::WaitWritable, Errno::EALREADY
  82. skipped @interests = :w
  83. skipped rescue ::IO::WaitReadable
  84. skipped @interests = :r
  85. skipped rescue Errno::EISCONN
  86. skipped transition(:connected)
  87. skipped @interests = :w
  88. skipped else
  89. skipped transition(:connected)
  90. skipped @interests = :w
  91. skipped end
  92. skipped private :try_connect
  93. skipped
  94. skipped def read(size, buffer)
  95. skipped @io.read_nonblock(size, buffer)
  96. skipped log { "READ: #{buffer.bytesize} bytes..." }
  97. skipped buffer.bytesize
  98. skipped rescue ::IO::WaitReadable
  99. skipped buffer.clear
  100. skipped 0
  101. skipped rescue EOFError
  102. skipped nil
  103. skipped end
  104. skipped
  105. skipped def write(buffer)
  106. skipped siz = @io.write_nonblock(buffer)
  107. skipped log { "WRITE: #{siz} bytes..." }
  108. skipped buffer.shift!(siz)
  109. skipped siz
  110. skipped rescue ::IO::WaitWritable
  111. skipped 0
  112. skipped rescue EOFError
  113. skipped nil
  114. skipped end
  115. skipped # :nocov:
  116. else
  117. 26 def try_connect
  118. 12840 case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
  119. when :wait_readable
  120. @interests = :r
  121. return
  122. when :wait_writable
  123. 6398 @interests = :w
  124. 6398 return
  125. end
  126. 6347 transition(:connected)
  127. 6347 @interests = :w
  128. rescue Errno::EALREADY
  129. 29 @interests = :w
  130. end
  131. 26 private :try_connect
  132. 26 def read(size, buffer)
  133. 45679 ret = @io.read_nonblock(size, buffer, exception: false)
  134. 45679 if ret == :wait_readable
  135. 4829 buffer.clear
  136. 4829 return 0
  137. end
  138. 40850 return if ret.nil?
  139. 40910 log { "READ: #{buffer.bytesize} bytes..." }
  140. 40841 buffer.bytesize
  141. end
  142. 26 def write(buffer)
  143. 19800 siz = @io.write_nonblock(buffer, exception: false)
  144. 19781 return 0 if siz == :wait_writable
  145. 19769 return if siz.nil?
  146. 19832 log { "WRITE: #{siz} bytes..." }
  147. 19769 buffer.shift!(siz)
  148. 19769 siz
  149. end
  150. end
  151. 26 def close
  152. 6334 return if @keep_open || closed?
  153. 2666 begin
  154. 6257 @io.close
  155. ensure
  156. 6257 transition(:closed)
  157. end
  158. end
  159. 26 def connected?
  160. 7645 @state == :connected
  161. end
  162. 26 def closed?
  163. 19703 @state == :idle || @state == :closed
  164. end
  165. skipped # :nocov:
  166. skipped def inspect
  167. skipped "#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"
  168. skipped end
  169. skipped # :nocov:
  170. 26 private
  171. 26 def build_socket
  172. 6414 @ip = @addresses[@ip_index]
  173. 6414 Socket.new(@ip.family, :STREAM, 0)
  174. end
  175. 26 def transition(nextstate)
  176. 11135 case nextstate
  177. # when :idle
  178. when :connected
  179. 3741 return unless @state == :idle
  180. when :closed
  181. 3619 return unless @state == :connected
  182. end
  183. 11135 do_transition(nextstate)
  184. end
  185. 26 def do_transition(nextstate)
  186. 21838 log(level: 1) { log_transition_state(nextstate) }
  187. 21705 @state = nextstate
  188. end
  189. 26 def log_transition_state(nextstate)
  190. 133 case nextstate
  191. when :connected
  192. 36 "Connected to #{host} (##{@io.fileno})"
  193. else
  194. 97 "#{host} #{@state} -> #{nextstate}"
  195. end
  196. end
  197. end
  198. end

lib/httpx/io/udp.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "ipaddr"
  3. 26 module HTTPX
  4. 26 class UDP
  5. 26 include Loggable
  6. 26 def initialize(ip, port, options)
  7. 635 @host = ip
  8. 635 @port = port
  9. 635 @io = UDPSocket.new(IPAddr.new(ip).family)
  10. 635 @options = options
  11. end
  12. 26 def to_io
  13. 390965 @io.to_io
  14. end
  15. 26 def connect; end
  16. 26 def connected?
  17. 635 true
  18. end
  19. 26 if RUBY_VERSION < "2.3"
  20. skipped # :nocov:
  21. skipped def close
  22. skipped @io.close
  23. skipped rescue StandardError
  24. skipped nil
  25. skipped end
  26. skipped # :nocov:
  27. else
  28. 26 def close
  29. 988 @io.close
  30. end
  31. end
  32. skipped # :nocov:
  33. skipped if (RUBY_ENGINE == "truffleruby" && RUBY_ENGINE_VERSION < "21.1.0") ||
  34. skipped RUBY_VERSION < "2.3"
  35. skipped def write(buffer)
  36. skipped siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s))
  37. skipped log { "WRITE: #{siz} bytes..." }
  38. skipped buffer.shift!(siz)
  39. skipped siz
  40. skipped rescue ::IO::WaitWritable
  41. skipped 0
  42. skipped rescue EOFError
  43. skipped nil
  44. skipped end
  45. skipped
  46. skipped def read(size, buffer)
  47. skipped data, _ = @io.recvfrom_nonblock(size)
  48. skipped buffer.replace(data)
  49. skipped log { "READ: #{buffer.bytesize} bytes..." }
  50. skipped buffer.bytesize
  51. skipped rescue ::IO::WaitReadable
  52. skipped 0
  53. skipped rescue IOError
  54. skipped end
  55. skipped else
  56. skipped
  57. skipped def write(buffer)
  58. skipped siz = @io.sendmsg_nonblock(buffer.to_s, 0, Socket.sockaddr_in(@port, @host.to_s), exception: false)
  59. skipped return 0 if siz == :wait_writable
  60. skipped return if siz.nil?
  61. skipped
  62. skipped log { "WRITE: #{siz} bytes..." }
  63. skipped
  64. skipped buffer.shift!(siz)
  65. skipped siz
  66. skipped end
  67. skipped
  68. skipped def read(size, buffer)
  69. skipped ret = @io.recvfrom_nonblock(size, 0, buffer, exception: false)
  70. skipped return 0 if ret == :wait_readable
  71. skipped return if ret.nil?
  72. skipped
  73. skipped log { "READ: #{buffer.bytesize} bytes..." }
  74. skipped
  75. skipped buffer.bytesize
  76. skipped rescue IOError
  77. skipped end
  78. skipped end
  79. skipped
  80. skipped # In JRuby, sendmsg_nonblock is not implemented
  81. skipped def write(buffer)
  82. skipped siz = @io.send(buffer.to_s, 0, @host, @port)
  83. skipped log { "WRITE: #{siz} bytes..." }
  84. skipped buffer.shift!(siz)
  85. skipped siz
  86. skipped end if RUBY_ENGINE == "jruby"
  87. skipped # :nocov:
  88. end
  89. end

lib/httpx/io/unix.rb

100.0% lines covered

33 relevant lines. 33 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 class UNIX < TCP
  4. 26 using URIExtensions
  5. 26 attr_reader :path
  6. 26 alias_method :host, :path
  7. 26 def initialize(origin, addresses, options)
  8. 32 @addresses = []
  9. 32 @hostname = origin.host
  10. 32 @state = :idle
  11. 32 @options = Options.new(options)
  12. 32 @fallback_protocol = @options.fallback_protocol
  13. 32 if @options.io
  14. 16 @io = case @options.io
  15. when Hash
  16. 8 @options.io[origin.authority]
  17. else
  18. 8 @options.io
  19. end
  20. 16 raise Error, "Given IO objects do not match the request authority" unless @io
  21. 16 @path = @io.path
  22. 16 @keep_open = true
  23. 16 @state = :connected
  24. else
  25. 16 if @options.transport_options
  26. skipped # :nocov:
  27. skipped warn ":transport_options is deprecated, use :addresses instead"
  28. skipped @path = @options.transport_options[:path]
  29. skipped # :nocov:
  30. else
  31. 16 @path = addresses.first
  32. end
  33. end
  34. 32 @io ||= build_socket
  35. end
  36. 26 def connect
  37. 24 return unless closed?
  38. 9 begin
  39. 24 if @io.closed?
  40. 8 transition(:idle)
  41. 8 @io = build_socket
  42. end
  43. 24 @io.connect_nonblock(Socket.sockaddr_un(@path))
  44. rescue Errno::EISCONN
  45. end
  46. 16 transition(:connected)
  47. rescue Errno::EINPROGRESS,
  48. Errno::EALREADY,
  49. ::IO::WaitReadable
  50. end
  51. skipped # :nocov:
  52. skipped def inspect
  53. skipped "#<#{self.class}(path: #{@path}): (state: #{@state})>"
  54. skipped end
  55. skipped # :nocov:
  56. 26 private
  57. 26 def build_socket
  58. 24 Socket.new(Socket::PF_UNIX, :STREAM, 0)
  59. end
  60. end
  61. end

lib/httpx/loggable.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 module HTTPX
  3. 26 module Loggable
  4. 23 COLORS = {
  5. 2 black: 30,
  6. red: 31,
  7. green: 32,
  8. yellow: 33,
  9. blue: 34,
  10. magenta: 35,
  11. cyan: 36,
  12. white: 37,
  13. }.freeze
  14. 12360 def log(level: @options.debug_level, color: nil, &msg)
  15. 336980 return unless @options.debug
  16. 1108 return unless @options.debug_level >= level
  17. 1108 debug_stream = @options.debug
  18. 1108 message = (+"" << msg.call << "\n")
  19. 1108 message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
  20. 1108 debug_stream << message
  21. end
  22. 26 if Exception.instance_methods.include?(:full_message)
  23. 88 def log_exception(ex, level: @options.debug_level, color: nil)
  24. 623 return unless @options.debug
  25. 12 return unless @options.debug_level >= level
  26. 24 log(level: level, color: color) { ex.full_message }
  27. end
  28. else
  29. 4 def log_exception(ex, level: @options.debug_level, color: nil)
  30. 162 return unless @options.debug
  31. 4 return unless @options.debug_level >= level
  32. 4 message = +"#{ex.message} (#{ex.class})"
  33. 4 message << "\n" << ex.backtrace.join("\n") unless ex.backtrace.nil?
  34. 8 log(level: level, color: color) { message }
  35. end
  36. end
  37. end
  38. end

lib/httpx/options.rb

89.19% lines covered

148 relevant lines. 132 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 26 require "socket"
  3. 26 module HTTPX
  4. 26 class Options
  5. 26 BUFFER_SIZE = 1 << 14
  6. 26 WINDOW_SIZE = 1 << 14 # 16K
  7. 26 MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
  8. 26 CONNECT_TIMEOUT = 60
  9. 26 OPERATION_TIMEOUT = 60
  10. 26 KEEP_ALIVE_TIMEOUT = 20
  11. 26 SETTINGS_TIMEOUT = 10
  12. 26 READ_TIMEOUT = WRITE_TIMEOUT = REQUEST_TIMEOUT = Float::INFINITY
  13. # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
  14. 7 ip_address_families = begin
  15. 26 list = Socket.ip_address_list
  16. 81 if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? && !a.ipv6_unique_local? }
  17. [Socket::AF_INET6, Socket::AF_INET]
  18. else
  19. 26 [Socket::AF_INET]
  20. end
  21. rescue NotImplementedError
  22. [Socket::AF_INET]
  23. end
  24. 6 DEFAULT_OPTIONS = {
  25. 44 :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
  26. 26 :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
  27. :ssl => {},
  28. :http2_settings => { settings_enable_push: 0 },
  29. :fallback_protocol => "http/1.1",
  30. :timeout => {
  31. connect_timeout: CONNECT_TIMEOUT,
  32. settings_timeout: SETTINGS_TIMEOUT,
  33. operation_timeout: OPERATION_TIMEOUT,
  34. keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
  35. read_timeout: READ_TIMEOUT,
  36. write_timeout: WRITE_TIMEOUT,
  37. request_timeout: REQUEST_TIMEOUT,
  38. },
  39. :headers => {},
  40. :window_size => WINDOW_SIZE,
  41. :buffer_size => BUFFER_SIZE,
  42. :body_threshold_size => MAX_BODY_THRESHOLD_SIZE,
  43. :request_class => Class.new(Request),
  44. :response_class => Class.new(Response),
  45. :headers_class => Class.new(Headers),
  46. :request_body_class => Class.new(Request::Body),
  47. :response_body_class => Class.new(Response::Body),
  48. :connection_class => Class.new(Connection),
  49. :options_class => Class.new(self),
  50. :transport => nil,
  51. :transport_options => nil,
  52. :addresses => nil,
  53. :persistent => false,
  54. 26 :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
  55. :resolver_options => { cache: true },
  56. :ip_families => ip_address_families,
  57. }.freeze
  58. begin
  59. module HashExtensions
  60. refine Hash do
  61. def >=(other)
  62. Hash[other] <= self
  63. end
  64. def <=(other)
  65. other = Hash[other]
  66. return false unless size <= other.size
  67. each do |k, v|
  68. v2 = other.fetch(k) { return false }
  69. return false unless v2 == v
  70. end
  71. true
  72. end
  73. end
  74. end
  75. using HashExtensions
  76. 26 end unless Hash.method_defined?(:>=)
  77. 26 class << self
  78. 26 def new(options = {})
  79. # let enhanced options go through
  80. 47030 return options if self == Options && options.class < self
  81. 36177 return options if options.is_a?(self)
  82. 18885 super
  83. end
  84. 26 def method_added(meth)
  85. 28723 super
  86. 28723 return unless meth =~ /^option_(.+)$/
  87. 6939 optname = Regexp.last_match(1).to_sym
  88. 6939 attr_reader(optname)
  89. end
  90. 26 def def_option(optname, *args, &block)
  91. 596 if args.empty? && !block
  92. 581 class_eval(<<-OUT, __FILE__, __LINE__ + 1)
  93. def option_#{optname}(v); v; end # def option_smth(v); v; end
  94. OUT
  95. 581 return
  96. end
  97. 15 deprecated_def_option(optname, *args, &block)
  98. end
  99. 26 def deprecated_def_option(optname, layout = nil, &interpreter)
  100. 15 warn "DEPRECATION WARNING: using `def_option(#{optname})` for setting options is deprecated. " \
  101. "Define module OptionsMethods and `def option_#{optname}(val)` instead."
  102. 15 if layout
  103. 9 class_eval(<<-OUT, __FILE__, __LINE__ + 1)
  104. def option_#{optname}(value) # def option_origin(v)
  105. #{layout} # URI(v)
  106. end # end
  107. OUT
  108. 5 elsif interpreter
  109. 6 define_method(:"option_#{optname}") do |value|
  110. 6 instance_exec(value, &interpreter)
  111. end
  112. end
  113. end
  114. end
  115. 26 def initialize(options = {})
  116. 18885 __initialize__(options)
  117. 18876 freeze
  118. end
  119. 26 def freeze
  120. 26817 super
  121. 26817 @origin.freeze
  122. 26817 @base_path.freeze
  123. 26817 @timeout.freeze
  124. 26817 @headers.freeze
  125. 26817 @addresses.freeze
  126. end
  127. 26 def option_origin(value)
  128. 1152 URI(value)
  129. end
  130. 26 def option_base_path(value)
  131. 36 String(value)
  132. end
  133. 26 def option_headers(value)
  134. 18885 Headers.new(value)
  135. end
  136. 26 def option_timeout(value)
  137. 18885 timeouts = Hash[value]
  138. 18885 if timeouts.key?(:loop_timeout)
  139. warn ":loop_timeout is deprecated, use :operation_timeout instead"
  140. timeouts[:operation_timeout] = timeouts.delete(:loop_timeout)
  141. end
  142. 18885 timeouts
  143. end
  144. 26 def option_max_concurrent_requests(value)
  145. 2157 raise TypeError, ":max_concurrent_requests must be positive" unless value.positive?
  146. 2157 value
  147. end
  148. 26 def option_max_requests(value)
  149. 676 raise TypeError, ":max_requests must be positive" unless value.positive?
  150. 676 value
  151. end
  152. 26 def option_window_size(value)
  153. 18885 value = Integer(value)
  154. 18885 raise TypeError, ":window_size must be positive" unless value.positive?
  155. 18885 value
  156. end
  157. 26 def option_buffer_size(value)
  158. 18885 value = Integer(value)
  159. 18885 raise TypeError, ":buffer_size must be positive" unless value.positive?
  160. 18885 value
  161. end
  162. 26 def option_body_threshold_size(value)
  163. 18885 Integer(value)
  164. end
  165. 26 def option_transport(value)
  166. 88 transport = value.to_s
  167. 88 raise TypeError, "#{transport} is an unsupported transport type" unless %w[unix].include?(transport)
  168. 88 transport
  169. end
  170. 26 def option_addresses(value)
  171. 48 Array(value)
  172. end
  173. 26 def option_ip_families(value)
  174. 18885 Array(value)
  175. end
  176. 19 %i[
  177. params form json xml body ssl http2_settings
  178. request_class response_class headers_class request_body_class
  179. response_body_class connection_class options_class
  180. io fallback_protocol debug debug_level transport_options resolver_class resolver_options
  181. persistent
  182. 7 ].each do |method_name|
  183. 572 def_option(method_name)
  184. end
  185. 26 REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
  186. 26 private_constant :REQUEST_IVARS
  187. 26 def ==(other)
  188. 3603 ivars = instance_variables | other.instance_variables
  189. 3603 ivars.all? do |ivar|
  190. 48912 case ivar
  191. when :@headers
  192. # currently, this is used to pick up an available matching connection.
  193. # the headers do not play a role, as they are relevant only for the request.
  194. 3253 true
  195. when *REQUEST_IVARS
  196. 290 true
  197. else
  198. 45369 instance_variable_get(ivar) == other.instance_variable_get(ivar)
  199. end
  200. end
  201. end
  202. <