Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ You can configure the client by passing options during initialization.
| `debug` | Boolean | `false` | Enable debug logging. |
| `on_error` | Proc | `nil` | Callback for handling errors. |
| `adapter` | Symbol | auto-detect | HTTP adapter to use (`:faraday`, `:httpx`, `:net_http`). |
| `proxy` | String, Boolean | adapter default | Proxy to use for requests. Pass a URI string to force a specific proxy, or `false` to disable proxying (including any `HTTP_PROXY`/`HTTPS_PROXY` environment variables) for a fixed, trusted `base_url`. Unset by default: the `:faraday` and `:net_http` adapters auto-detect a proxy from the environment as they normally would; `:httpx` never does. |

## License

Expand Down
42 changes: 33 additions & 9 deletions lib/altertable/adapters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ def post(path, body: nil, params: {}, &block)
end

class FaradayAdapter < Base
def initialize(base_url:, timeout:, headers: {})
super
# proxy left unset (default) follows Faraday's own env-based auto-detection
# (HTTP_PROXY/HTTPS_PROXY/NO_PROXY). Pass `false` to disable proxying entirely for
# fixed, trusted destinations, or a URI string to force a specific proxy.
def initialize(base_url:, timeout:, headers: {}, proxy: nil)
super(base_url: base_url, timeout: timeout, headers: headers)
require "faraday"

@conn = Faraday.new(url: @base_url) do |f|
@headers.each { |k, v| f.headers[k] = v }
f.options.timeout = @timeout
f.proxy = proxy unless proxy.nil?
f.adapter Faraday.default_adapter
end
end
Expand All @@ -46,10 +50,14 @@ def wrap_response(resp)
end

class HttpxAdapter < Base
def initialize(base_url:, timeout:, headers: {})
super
# This adapter never auto-detects HTTP_PROXY/HTTPS_PROXY (httpx requires the :proxy
# plugin to be loaded explicitly). `proxy: false` and the default (unset) are
# therefore equivalent; pass a URI string to force a specific proxy.
def initialize(base_url:, timeout:, headers: {}, proxy: nil)
super(base_url: base_url, timeout: timeout, headers: headers)
require "httpx"
@client = HTTPX.plugin(:retries).with(
client = proxy ? HTTPX.plugin(:proxy).plugin(:retries).with(proxy: { uri: proxy }) : HTTPX.plugin(:retries)
@client = client.with(
timeout: { operation_timeout: @timeout },
headers: @headers,
base_url: @base_url
Expand All @@ -74,11 +82,15 @@ def wrap_response(resp)
end

class NetHttpAdapter < Base
def initialize(base_url:, timeout:, headers: {})
super
# proxy left unset (default) follows Net::HTTP's own env-based auto-detection
# (HTTP_PROXY/HTTPS_PROXY/NO_PROXY). Pass `false` to disable proxying entirely for
# fixed, trusted destinations, or a URI string to force a specific proxy.
def initialize(base_url:, timeout:, headers: {}, proxy: nil)
super(base_url: base_url, timeout: timeout, headers: headers)
require "net/http"
require "uri"
@uri = URI.parse(@base_url)
@proxy = proxy
end

def post(path, body: nil, params: {})
Expand All @@ -89,7 +101,7 @@ def post(path, body: nil, params: {})
@headers.each { |k, v| req[k] = v }
req.body = body if body

Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
Net::HTTP.start(uri.host, uri.port, *proxy_start_args, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
resp = http.request(req)
Response.new(resp.code.to_i, resp.body, resp.to_hash)
end
Expand All @@ -98,6 +110,18 @@ def post(path, body: nil, params: {})
rescue StandardError => e
raise Altertable::NetworkError.new(e.message, e)
end

private

# Net::HTTP.start's p_addr defaults to :ENV; returning [] here preserves that.
# Passing an explicit nil p_addr (i.e. [nil]) is how Net::HTTP disables proxying.
def proxy_start_args
return [] if @proxy.nil?
return [nil] if @proxy == false

proxy_uri = URI.parse(@proxy)
[proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
end
end
end
end
2 changes: 1 addition & 1 deletion lib/altertable/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def initialize(api_key, options = {})
"X-API-Key" => @api_key,
"Content-Type" => "application/json"
}
@adapter = select_adapter(adapter_name, { base_url: @base_url, timeout: @timeout, headers: headers })
@adapter = select_adapter(adapter_name, { base_url: @base_url, timeout: @timeout, headers: headers, proxy: options[:proxy] })
end

def track(event, distinct_id, **options)
Expand Down
17 changes: 11 additions & 6 deletions rbi/altertable.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ module Altertable
end

class FaradayAdapter < Base
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
def initialize(base_url:, timeout:, headers: nil); end
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String]), proxy: T.nilable(T.any(String, T::Boolean))).void }
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end

sig { params(path: String, body: T.nilable(String), params: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]), block: T.nilable(T.proc.params(arg0: T.untyped).void)).returns(Altertable::Adapters::Response) }
def post(path, body: nil, params: nil, &block); end
Expand All @@ -95,8 +95,8 @@ module Altertable
end

class HttpxAdapter < Base
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
def initialize(base_url:, timeout:, headers: nil); end
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String]), proxy: T.nilable(T.any(String, T::Boolean))).void }
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end

sig { params(path: String, body: T.nilable(String), params: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]), block: T.nilable(T.proc.params(arg0: T.untyped).void)).returns(Altertable::Adapters::Response) }
def post(path, body: nil, params: nil, &block); end
Expand All @@ -108,11 +108,16 @@ module Altertable
end

class NetHttpAdapter < Base
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
def initialize(base_url:, timeout:, headers: nil); end
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String]), proxy: T.nilable(T.any(String, T::Boolean))).void }
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end

sig { params(path: String, body: T.nilable(String), params: T.nilable(T::Hash[T.any(Symbol, String), T.untyped]), block: T.nilable(T.proc.params(arg0: T.untyped).void)).returns(Altertable::Adapters::Response) }
def post(path, body: nil, params: nil, &block); end

private

sig { returns(T::Array[T.untyped]) }
def proxy_start_args; end
end
end

Expand Down
11 changes: 8 additions & 3 deletions sig/altertable/adapters.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module Altertable
class FaradayAdapter < Base
@conn: untyped

def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String]) -> void
def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String], ?proxy: (String | bool)?) -> void
def post: (String path, ?body: String?, ?params: ::Hash[Symbol | String, untyped]) -> Response

private
Expand All @@ -30,7 +30,7 @@ module Altertable
class HttpxAdapter < Base
@client: untyped

def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String]) -> void
def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String], ?proxy: (String | bool)?) -> void
def post: (String path, ?body: String?, ?params: ::Hash[Symbol | String, untyped]) -> Response

private
Expand All @@ -40,9 +40,14 @@ module Altertable

class NetHttpAdapter < Base
@uri: URI::HTTP | URI::HTTPS
@proxy: (String | bool)?

def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String]) -> void
def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String], ?proxy: (String | bool)?) -> void
def post: (String path, ?body: String?, ?params: ::Hash[Symbol | String, untyped]) -> Response

private

def proxy_start_args: () -> ::Array[untyped]
end
end
end
8 changes: 8 additions & 0 deletions spec/adapter_selection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,13 @@
expect(adapter).to be_a(Altertable::Adapters::NetHttpAdapter)
end
end

context "when proxy is provided" do
it "threads proxy: false through to the adapter" do
client = described_class.new(api_key, base_url: base_url, adapter: :net_http, proxy: false)
adapter = client.instance_variable_get(:@adapter)
expect(adapter.send(:proxy_start_args)).to eq([nil])
end
end
end
end
34 changes: 34 additions & 0 deletions spec/altertable/adapters/faraday_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Altertable::Adapters::FaradayAdapter do
let(:base_url) { "https://api.test.local" }
let(:timeout) { 5 }

describe "proxy configuration" do
it "leaves Faraday's own env-based auto-detection in place by default" do
adapter = described_class.new(base_url: base_url, timeout: timeout)
conn = adapter.instance_variable_get(:@conn)
expect(conn.instance_variable_get(:@manual_proxy)).to be false
end

it "disables Faraday's env-based auto-detection when proxy: false is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
conn = adapter.instance_variable_get(:@conn)
expect(conn.instance_variable_get(:@manual_proxy)).to be true
end

it "has no proxy configured when proxy: false is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
conn = adapter.instance_variable_get(:@conn)
expect(conn.proxy).to be_nil
end

it "uses an explicit proxy when a URI string is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://proxy.local:8080")
conn = adapter.instance_variable_get(:@conn)
expect(conn.proxy.uri.host).to eq("proxy.local")
end
end
end
34 changes: 34 additions & 0 deletions spec/altertable/adapters/httpx_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Altertable::Adapters::HttpxAdapter do
let(:base_url) { "https://api.test.local" }
let(:timeout) { 5 }

describe "proxy configuration" do
# Introspects the httpx client's actual proxy option rather than the class name:
# httpx's plugin system only gives its dynamically generated classes readable
# names on Ruby >= 3.4 (via Class#set_temporary_name), so `client.class.to_s`
# is anonymous (e.g. "#<Class:0x...>") on older Rubies and can't be asserted on.
def proxy_option(adapter)
options = adapter.instance_variable_get(:@client).instance_variable_get(:@options)
options.proxy if options.respond_to?(:proxy)
end

it "does not proxy by default (httpx never auto-detects env proxies)" do
adapter = described_class.new(base_url: base_url, timeout: timeout)
expect(proxy_option(adapter)).to be_nil
end

it "does not proxy when proxy: false is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
expect(proxy_option(adapter)).to be_nil
end

it "uses an explicit proxy when a URI string is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://proxy.local:8080")
expect(proxy_option(adapter).uri.to_s).to eq("http://proxy.local:8080")
end
end
end
25 changes: 25 additions & 0 deletions spec/altertable/adapters/net_http_adapter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Altertable::Adapters::NetHttpAdapter do
let(:base_url) { "https://api.test.local" }
let(:timeout) { 5 }

describe "proxy configuration" do
it "leaves Net::HTTP's own env-based auto-detection in place by default" do
adapter = described_class.new(base_url: base_url, timeout: timeout)
expect(adapter.send(:proxy_start_args)).to eq([])
end

it "disables proxying when proxy: false is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
expect(adapter.send(:proxy_start_args)).to eq([nil])
end

it "uses an explicit proxy when a URI string is given" do
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://user:pass@proxy.local:8080")
expect(adapter.send(:proxy_start_args)).to eq(["proxy.local", 8080, "user", "pass"])
end
end
end
Loading