Module: OnyxCord::Internal::HTTP

Defined in:
lib/onyxcord/internal/http.rb

Overview

Modern HTTP adapter wrapping HTTPX with persistent connections, automatic retries on 502, and a response interface compatible with the rest of OnyxCord.

Defined Under Namespace

Classes: Response

Constant Summary collapse

POOL_OPTIONS =
{
  max_connections: 50,
  max_connections_per_origin: 20,
  pool_timeout: 10
}.freeze

Class Method Summary collapse

Class Method Details

.delete(url, **headers) ⇒ Object



217
218
219
# File 'lib/onyxcord/internal/http.rb', line 217

def delete(url, **headers)
  request(:delete, url, nil, **headers)
end

.get(url, **headers) ⇒ Object

Convenience wrappers



201
202
203
# File 'lib/onyxcord/internal/http.rb', line 201

def get(url, **headers)
  request(:get, url, nil, **headers)
end

.multipart?(body) ⇒ Boolean

Returns:

  • (Boolean)


181
182
183
184
185
# File 'lib/onyxcord/internal/http.rb', line 181

def multipart?(body)
  return true if body.is_a?(Array) && body.all? { |part| part.is_a?(Hash) && part[:name] && part.key?(:value) }

  body.is_a?(Hash) && body.any? { |_, value| value.respond_to?(:read) || value.respond_to?(:path) }
end

.multipart_body(body, boundary) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/onyxcord/internal/http.rb', line 137

def multipart_body(body, boundary)
  output = String.new(encoding: Encoding::BINARY)

  multipart_parts(body).each do |part|
    key = part[:name]
    value = part[:value]
    output << "--#{boundary}\r\n"
    if part[:filename]
      value.rewind if value.respond_to?(:rewind)
      filename = part[:filename]
      output << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"\r\n"
      output << "Content-Type: #{part[:content_type] || multipart_content_type(filename)}\r\n\r\n"
      output << value.read.to_s
    else
      output << "Content-Disposition: form-data; name=\"#{key}\"\r\n"
      output << "\r\n"
      output << value.to_s
    end
    output << "\r\n"
  end

  output << "--#{boundary}--\r\n"
end

.multipart_content_type(filename) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/onyxcord/internal/http.rb', line 161

def multipart_content_type(filename)
  ext = File.extname(filename).downcase
  case ext
  when '.txt' then 'text/plain'
  when '.json' then 'application/json'
  when '.xml' then 'application/xml'
  when '.png' then 'image/png'
  when '.jpg', '.jpeg' then 'image/jpeg'
  when '.gif' then 'image/gif'
  when '.webp' then 'image/webp'
  when '.mp3' then 'audio/mpeg'
  when '.mp4' then 'video/mp4'
  when '.ogg' then 'audio/ogg'
  when '.wav' then 'audio/wav'
  when '.pdf' then 'application/pdf'
  when '.zip' then 'application/zip'
  else 'application/octet-stream'
  end
end

.multipart_parts(body) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/onyxcord/internal/http.rb', line 187

def multipart_parts(body)
  return body if body.is_a?(Array)

  body.map do |key, value|
    if value.respond_to?(:read) || value.respond_to?(:path)
      upload = Upload.wrap(value)
      { name: key, value: upload, filename: upload.filename, content_type: upload.content_type }
    else
      { name: key, value: value }
    end
  end
end

.patch(url, body = nil, **headers) ⇒ Object



213
214
215
# File 'lib/onyxcord/internal/http.rb', line 213

def patch(url, body = nil, **headers)
  request(:patch, url, body, **headers)
end

.post(url, body = nil, **headers) ⇒ Object



205
206
207
# File 'lib/onyxcord/internal/http.rb', line 205

def post(url, body = nil, **headers)
  request(:post, url, body, **headers)
end

.post_multipart(url, body, headers) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/onyxcord/internal/http.rb', line 124

def post_multipart(url, body, headers)
  uri = URI(url)
  boundary = "----OnyxCord#{SecureRandom.hex(12)}"
  request = Net::HTTP::Post.new(uri)
  headers.each { |key, value| request[key.to_s] = value }
  request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
  request.body = multipart_body(body, boundary)

  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(request)
  end
end

.put(url, body = nil, **headers) ⇒ Object



209
210
211
# File 'lib/onyxcord/internal/http.rb', line 209

def put(url, body = nil, **headers)
  request(:put, url, body, **headers)
end

.request(type, url, body = nil, **headers) ⇒ Response

Perform a raw HTTP request and return a Response.

Parameters:

  • type (Symbol)

    HTTP method (:get, :post, :put, :patch, :delete)

  • url (String)

    The full URL.

  • body (String, Hash, nil) (defaults to: nil)

    The request body.

  • headers (Hash)

    Request headers.

Returns:



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
120
121
122
# File 'lib/onyxcord/internal/http.rb', line 95

def request(type, url, body = nil, **headers)
  http = session.with(headers: headers)

  raw = case type
        when :get
          http.get(url)
        when :post
          if multipart?(body)
            # Multipart upload
            post_multipart(url, body, headers)
          else
            http.post(url, body: body)
          end
        when :put
          http.put(url, body: body)
        when :patch
          http.patch(url, body: body)
        when :delete
          http.delete(url)
        else
          raise ArgumentError, "Unknown HTTP method: #{type}"
        end

  # HTTPX returns an error response object on network failures
  raise raw.error if raw.is_a?(HTTPX::ErrorResponse)

  Response.new(raw)
end

.reset!Object

Reset the HTTP session (useful for tests).



77
78
79
80
81
82
83
# File 'lib/onyxcord/internal/http.rb', line 77

def reset!
  session_mutex.synchronize do
    @session&.close if @session.respond_to?(:close)
  ensure
    @session = nil
  end
end

.sessionObject

The shared HTTPX session with persistent, pooled connections.



64
65
66
67
68
69
70
71
72
73
74
# File 'lib/onyxcord/internal/http.rb', line 64

def session
  session_mutex.synchronize do
    @session ||= HTTPX.plugin(:persistent)
                      .plugin(:follow_redirects)
                      .with(
                        fallback_protocol: 'http/1.1',
                        ssl: { alpn_protocols: ['http/1.1'] },
                        pool_options: POOL_OPTIONS
                      )
  end
end

.session_mutexObject



85
86
87
# File 'lib/onyxcord/internal/http.rb', line 85

def session_mutex
  @session_mutex ||= Mutex.new
end