Skip to content

Commit 53f17cf

Browse files
leonkennethclaude
andcommitted
feat(adapters): support explicit proxy configuration
Add a `proxy` client option that is threaded through to the Faraday, httpx, and net/http adapters. Passing a URI string forces a specific proxy; passing `false` disables proxying (including env-based auto-detection) for a fixed, trusted `base_url`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1xdiN28Drqsrh3rET88kW
1 parent 7d6b4f7 commit 53f17cf

9 files changed

Lines changed: 149 additions & 19 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ You can configure the client by passing options during initialization.
9292
| `debug` | Boolean | `false` | Enable debug logging. |
9393
| `on_error` | Proc | `nil` | Callback for handling errors. |
9494
| `adapter` | Symbol | auto-detect | HTTP adapter to use (`:faraday`, `:httpx`, `:net_http`). |
95+
| `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. |
9596

9697
## License
9798

‎lib/altertable/adapters.rb‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ def post(path, body: nil, params: {}, &block)
1515
end
1616

1717
class FaradayAdapter < Base
18-
def initialize(base_url:, timeout:, headers: {})
19-
super
18+
# proxy left unset (default) follows Faraday's own env-based auto-detection
19+
# (HTTP_PROXY/HTTPS_PROXY/NO_PROXY). Pass `false` to disable proxying entirely for
20+
# fixed, trusted destinations, or a URI string to force a specific proxy.
21+
def initialize(base_url:, timeout:, headers: {}, proxy: nil)
22+
super(base_url: base_url, timeout: timeout, headers: headers)
2023
require "faraday"
21-
24+
2225
@conn = Faraday.new(url: @base_url) do |f|
2326
@headers.each { |k, v| f.headers[k] = v }
2427
f.options.timeout = @timeout
28+
f.proxy = proxy unless proxy.nil?
2529
f.adapter Faraday.default_adapter
2630
end
2731
end
@@ -46,10 +50,14 @@ def wrap_response(resp)
4650
end
4751

4852
class HttpxAdapter < Base
49-
def initialize(base_url:, timeout:, headers: {})
50-
super
53+
# This adapter never auto-detects HTTP_PROXY/HTTPS_PROXY (httpx requires the :proxy
54+
# plugin to be loaded explicitly). `proxy: false` and the default (unset) are
55+
# therefore equivalent; pass a URI string to force a specific proxy.
56+
def initialize(base_url:, timeout:, headers: {}, proxy: nil)
57+
super(base_url: base_url, timeout: timeout, headers: headers)
5158
require "httpx"
52-
@client = HTTPX.plugin(:retries).with(
59+
client = proxy ? HTTPX.plugin(:proxy).plugin(:retries).with(proxy: { uri: proxy }) : HTTPX.plugin(:retries)
60+
@client = client.with(
5361
timeout: { operation_timeout: @timeout },
5462
headers: @headers,
5563
base_url: @base_url
@@ -74,11 +82,15 @@ def wrap_response(resp)
7482
end
7583

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

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

92-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
104+
Net::HTTP.start(uri.host, uri.port, *proxy_start_args, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
93105
resp = http.request(req)
94106
Response.new(resp.code.to_i, resp.body, resp.to_hash)
95107
end
@@ -98,6 +110,18 @@ def post(path, body: nil, params: {})
98110
rescue StandardError => e
99111
raise Altertable::NetworkError.new(e.message, e)
100112
end
113+
114+
private
115+
116+
# Net::HTTP.start's p_addr defaults to :ENV; returning [] here preserves that.
117+
# Passing an explicit nil p_addr (i.e. [nil]) is how Net::HTTP disables proxying.
118+
def proxy_start_args
119+
return [] if @proxy.nil?
120+
return [nil] if @proxy == false
121+
122+
proxy_uri = URI.parse(@proxy)
123+
[proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
124+
end
101125
end
102126
end
103127
end

‎lib/altertable/client.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def initialize(api_key, options = {})
2828
"X-API-Key" => @api_key,
2929
"Content-Type" => "application/json"
3030
}
31-
@adapter = select_adapter(adapter_name, { base_url: @base_url, timeout: @timeout, headers: headers })
31+
@adapter = select_adapter(adapter_name, { base_url: @base_url, timeout: @timeout, headers: headers, proxy: options[:proxy] })
3232
end
3333

3434
def track(event, distinct_id, **options)

‎rbi/altertable.rbi‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ module Altertable
8282
end
8383

8484
class FaradayAdapter < Base
85-
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
86-
def initialize(base_url:, timeout:, headers: nil); end
85+
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 }
86+
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end
8787

8888
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) }
8989
def post(path, body: nil, params: nil, &block); end
@@ -95,8 +95,8 @@ module Altertable
9595
end
9696

9797
class HttpxAdapter < Base
98-
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
99-
def initialize(base_url:, timeout:, headers: nil); end
98+
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 }
99+
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end
100100

101101
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) }
102102
def post(path, body: nil, params: nil, &block); end
@@ -108,11 +108,16 @@ module Altertable
108108
end
109109

110110
class NetHttpAdapter < Base
111-
sig { params(base_url: String, timeout: T.any(Integer, Float), headers: T.nilable(T::Hash[String, String])).void }
112-
def initialize(base_url:, timeout:, headers: nil); end
111+
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 }
112+
def initialize(base_url:, timeout:, headers: nil, proxy: nil); end
113113

114114
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) }
115115
def post(path, body: nil, params: nil, &block); end
116+
117+
private
118+
119+
sig { returns(T::Array[T.untyped]) }
120+
def proxy_start_args; end
116121
end
117122
end
118123

‎sig/altertable/adapters.rbs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module Altertable
1919
class FaradayAdapter < Base
2020
@conn: untyped
2121

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

2525
private
@@ -30,7 +30,7 @@ module Altertable
3030
class HttpxAdapter < Base
3131
@client: untyped
3232

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

3636
private
@@ -40,9 +40,14 @@ module Altertable
4040

4141
class NetHttpAdapter < Base
4242
@uri: URI::HTTP | URI::HTTPS
43+
@proxy: (String | bool)?
4344

44-
def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String]) -> void
45+
def initialize: (base_url: String, timeout: Integer | Float, ?headers: ::Hash[String, String], ?proxy: (String | bool)?) -> void
4546
def post: (String path, ?body: String?, ?params: ::Hash[Symbol | String, untyped]) -> Response
47+
48+
private
49+
50+
def proxy_start_args: () -> ::Array[untyped]
4651
end
4752
end
4853
end

‎spec/adapter_selection_spec.rb‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,13 @@
2626
expect(adapter).to be_a(Altertable::Adapters::NetHttpAdapter)
2727
end
2828
end
29+
30+
context "when proxy is provided" do
31+
it "threads proxy: false through to the adapter" do
32+
client = described_class.new(api_key, base_url: base_url, adapter: :net_http, proxy: false)
33+
adapter = client.instance_variable_get(:@adapter)
34+
expect(adapter.send(:proxy_start_args)).to eq([nil])
35+
end
36+
end
2937
end
3038
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Altertable::Adapters::FaradayAdapter do
6+
let(:base_url) { "https://api.test.local" }
7+
let(:timeout) { 5 }
8+
9+
describe "proxy configuration" do
10+
it "leaves Faraday's own env-based auto-detection in place by default" do
11+
adapter = described_class.new(base_url: base_url, timeout: timeout)
12+
conn = adapter.instance_variable_get(:@conn)
13+
expect(conn.instance_variable_get(:@manual_proxy)).to be false
14+
end
15+
16+
it "disables Faraday's env-based auto-detection when proxy: false is given" do
17+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
18+
conn = adapter.instance_variable_get(:@conn)
19+
expect(conn.instance_variable_get(:@manual_proxy)).to be true
20+
end
21+
22+
it "has no proxy configured when proxy: false is given" do
23+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
24+
conn = adapter.instance_variable_get(:@conn)
25+
expect(conn.proxy).to be_nil
26+
end
27+
28+
it "uses an explicit proxy when a URI string is given" do
29+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://proxy.local:8080")
30+
conn = adapter.instance_variable_get(:@conn)
31+
expect(conn.proxy.uri.host).to eq("proxy.local")
32+
end
33+
end
34+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Altertable::Adapters::HttpxAdapter do
6+
let(:base_url) { "https://api.test.local" }
7+
let(:timeout) { 5 }
8+
9+
describe "proxy configuration" do
10+
it "does not proxy by default (httpx never auto-detects env proxies)" do
11+
adapter = described_class.new(base_url: base_url, timeout: timeout)
12+
client = adapter.instance_variable_get(:@client)
13+
expect(client.class.to_s).not_to include("Plugins::Proxy")
14+
end
15+
16+
it "does not proxy when proxy: false is given" do
17+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
18+
client = adapter.instance_variable_get(:@client)
19+
expect(client.class.to_s).not_to include("Plugins::Proxy")
20+
end
21+
22+
it "uses an explicit proxy when a URI string is given" do
23+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://proxy.local:8080")
24+
client = adapter.instance_variable_get(:@client)
25+
expect(client.class.to_s).to include("Plugins::Proxy")
26+
end
27+
end
28+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Altertable::Adapters::NetHttpAdapter do
6+
let(:base_url) { "https://api.test.local" }
7+
let(:timeout) { 5 }
8+
9+
describe "proxy configuration" do
10+
it "leaves Net::HTTP's own env-based auto-detection in place by default" do
11+
adapter = described_class.new(base_url: base_url, timeout: timeout)
12+
expect(adapter.send(:proxy_start_args)).to eq([])
13+
end
14+
15+
it "disables proxying when proxy: false is given" do
16+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: false)
17+
expect(adapter.send(:proxy_start_args)).to eq([nil])
18+
end
19+
20+
it "uses an explicit proxy when a URI string is given" do
21+
adapter = described_class.new(base_url: base_url, timeout: timeout, proxy: "http://user:pass@proxy.local:8080")
22+
expect(adapter.send(:proxy_start_args)).to eq(["proxy.local", 8080, "user", "pass"])
23+
end
24+
end
25+
end

0 commit comments

Comments
 (0)