Class: Hypertube::Core::Http2Client::Http2Client

Inherits:
Object
  • Object
show all
Defined in:
lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb

Defined Under Namespace

Classes: Http2TransportError

Constant Summary collapse

MAX_ATTEMPTS =
2
DEFAULT_CONNECT_TIMEOUT_SECONDS =
120
DEFAULT_RESPONSE_TIMEOUT_SECONDS =
180
RESTRICTED_HTTP2_HEADERS =
%w[
  connection
  keep-alive
  proxy-connection
  transfer-encoding
  upgrade
  host
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_data) ⇒ Http2Client

Returns a new instance of Http2Client.



120
121
122
123
124
125
126
127
128
129
130
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 120

def initialize(connection_data)
  @connection_data = connection_data
  @send_receive_lock = Mutex.new
  @url = connection_data.respond_to?(:hostname) ? connection_data.hostname : connection_data.to_s
  @uri = URI.parse(@url)
  unless %w[http https].include?(@uri.scheme.to_s.downcase)
    raise "Http2Client requires 'http' (h2c) or 'https' (h2 over TLS) scheme. Provided URL: #{@url}"
  end
  @response_timeout = connection_data.respond_to?(:response_timeout) ? connection_data.response_timeout : DEFAULT_RESPONSE_TIMEOUT_SECONDS
  @http_client = build_http_client
end

Class Method Details

.close(connection_data) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 59

def close(connection_data)
  client = nil
  @clients_lock.synchronize do
    client = @clients.delete(connection_key(connection_data))
  end
  client&.close
end

.get_state(connection_data) ⇒ Object



67
68
69
70
71
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 67

def get_state(connection_data)
  @clients_lock.synchronize do
    @clients.key?(connection_key(connection_data)) ? :OPEN : nil
  end
end

.send_message(connection_data, message) ⇒ Object

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 29

def send_message(connection_data, message)
  raise ArgumentError, 'connection_data cannot be nil' if connection_data.nil?
  raise ArgumentError, 'message cannot be nil' if message.nil?

  client = create_or_get_client(connection_data, expected_current: nil)
  last_ex = nil

  MAX_ATTEMPTS.times do |attempt|
    begin
      client.with_send_receive_lock do
        return client.send_bytes(message)
      end
    rescue StandardError => ex
      last_ex = ex
      break if attempt == MAX_ATTEMPTS - 1
      break unless transient_http2_error?(ex)

      client = create_or_get_client(connection_data, expected_current: client)
    end
  end

  if last_ex
    e = RuntimeError.new("HTTP/2 send to #{connection_key(connection_data)} failed after #{MAX_ATTEMPTS} attempts. Last error: #{last_ex.message}")
    e.set_backtrace(last_ex.backtrace)
    raise e
  end

  raise RuntimeError, "HTTP/2 send to #{connection_key(connection_data)} failed after #{MAX_ATTEMPTS} attempts."
end

Instance Method Details

#closeObject



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 186

def close
  return unless @http_client

  if @http_client.respond_to?(:close)
    @http_client.close
  elsif @http_client.respond_to?(:shutdown)
    @http_client.shutdown
  end
rescue StandardError
  # Best-effort transport cleanup only; callers handle send errors.
ensure
  @http_client = nil
end

#send_bytes(message) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 136

def send_bytes(message)
  payload = normalize_payload(message)
  response = nil
  ensure_server_reachable!
  Timeout.timeout(operation_timeout_seconds) do
    response = @http_client.post(@url, headers: request_headers, body: payload)

    if defined?(HTTPX::ErrorResponse) && response.is_a?(HTTPX::ErrorResponse)
      response_error = response.respond_to?(:error) ? response.error : nil
      details = response_error ? response_error.message : response.to_s
      raise Http2TransportError, "HTTP/2 request to #{@url} failed: #{details}"
    end

    status_code = response.status.to_i
    http_version = response.version.to_s
    if !http_version.empty? && !http_version.start_with?('2')
      raise RuntimeError, "HTTP/2 request to #{@url} failed: the endpoint did not negotiate HTTP/2."
    end

    reason = extract_reason_header(response)
    if status_code < 200 || status_code >= 300
      raise RuntimeError, "HTTP/2 request to #{@url} failed with status #{status_code} #{reason}. "
    end

    return response.body.to_s.bytes
  end
rescue Timeout::Error
  timeout_text =
    if @response_timeout && @response_timeout >= 0
      "timed out after #{@response_timeout} seconds"
    else
      "timed out"
    end
  raise Http2TransportError, "HTTP/2 request to #{@url} #{timeout_text}. The server may not be responding."
rescue StandardError => e
  if timeout_error?(e)
    timeout_text =
      if @response_timeout && @response_timeout >= 0
        "timed out after #{@response_timeout} seconds"
      else
        "timed out"
      end
    raise Http2TransportError, "HTTP/2 request to #{@url} #{timeout_text}. The server may not be responding."
  end
  if defined?(HTTPX::Error) && e.is_a?(HTTPX::Error)
    raise Http2TransportError, "HTTP/2 request to #{@url} failed: #{e.message}. The server may be unavailable or not responding."
  end
  raise
end

#with_send_receive_lockObject



132
133
134
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 132

def with_send_receive_lock
  @send_receive_lock.synchronize { yield }
end