Class: LittleGhost::Providers::HTTPTransport

Inherits:
Object
  • Object
show all
Defined in:
lib/little_ghost/providers/http_transport.rb

Overview

HTTPTransport gives provider clients a shared, bounded streaming HTTP layer. It applies cancellation, deadlines, timeouts, and response-size limits while yielding response chunks as they arrive.

Security and trust

HTTPS is required by default. Enabling allow_insecure_http can expose API keys and model content in transit; use it only with a trusted local development endpoint.

Constant Summary collapse

DEFAULT_MAX_RESPONSE_BYTES =

Default upper bound for a complete provider response (50 MiB).

50 * 1024 * 1024
DEFAULT_MAX_ERROR_BODY_BYTES =

:nodoc:

4 * 1024
TRANSIENT_NETWORK_ERRORS =
[
  Net::OpenTimeout,
  Net::ReadTimeout,
  Net::WriteTimeout,
  EOFError,
  SocketError,
  SystemCallError,
  IOError,
  OpenSSL::SSL::SSLError,
  Net::ProtocolError,
  Net::HTTPBadResponse,
  Net::HTTPHeaderSyntaxError
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(base_url:, open_timeout:, read_timeout:, allow_insecure_http: false, max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES, max_error_body_bytes: DEFAULT_MAX_ERROR_BODY_BYTES) ⇒ HTTPTransport

Configures base_url with connection, read, and response size limits.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/little_ghost/providers/http_transport.rb', line 56

def initialize(
  base_url:,
  open_timeout:,
  read_timeout:,
  allow_insecure_http: false,
  max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
  max_error_body_bytes: DEFAULT_MAX_ERROR_BODY_BYTES
)
  @base_url = URI(base_url.end_with?("/") ? base_url : "#{base_url}/")
  unless %w[http https].include?(@base_url.scheme) && @base_url.host
    raise ConfigurationError, "Provider base_url must be an HTTP(S) URL"
  end
  if @base_url.scheme == "http" && !allow_insecure_http
    raise ConfigurationError, "Provider base_url must use HTTPS unless allow_insecure_http is enabled"
  end

  @open_timeout = open_timeout
  @read_timeout = read_timeout
  @max_response_bytes = positive_integer(max_response_bytes, :max_response_bytes)
  @max_error_body_bytes = positive_integer(max_error_body_bytes, :max_error_body_bytes)
end

Instance Method Details

#stream(path:, headers:, body:, cancellation_token:, deadline: nil) ⇒ Object

Posts body and yields response chunks until completion.

Cancellation and deadline interrupt the request. Without a block, this method returns an Enumerator.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/little_ghost/providers/http_transport.rb', line 82

def stream(path:, headers:, body:, cancellation_token:, deadline: nil)
  return enum_for(__method__, path:, headers:, body:, cancellation_token:, deadline:) unless block_given?

  stream = Support::InterruptibleStream.new(cancellation_token:, deadline:) do |emit|
    uri = URI.join(@base_url.to_s, path.sub(%r{\A/}, ""))
    request = Net::HTTP::Post.new(uri)
    headers.each { |name, value| request[name] = value }
    request.body = body

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == "https"
    http.open_timeout = remaining_timeout(deadline, @open_timeout)
    http.read_timeout = remaining_timeout(deadline, @read_timeout)
    http.write_timeout = remaining_timeout(deadline, @read_timeout)

    http.request(request) do |response|
      unless response.is_a?(Net::HTTPSuccess)
        response_body = read_limited(response, @max_error_body_bytes)
        raise HTTPError.new(
          "Provider request failed with HTTP #{response.code}",
          status: response.code.to_i,
          body: response_body
        )
      end

      bytes_read = 0
      response.read_body do |chunk|
        bytes_read += chunk.bytesize
        raise ProtocolError, "Provider response exceeded #{@max_response_bytes} bytes" if bytes_read > @max_response_bytes

        emit.call(chunk)
      end
    end
  rescue *TRANSIENT_NETWORK_ERRORS => error
    raise HTTPError, "Provider request failed (#{error.class})"
  end
  stream.each { |chunk| yield chunk }
end