diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d729c7..4c014f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Babashka [http-client](https://github.com/babashka/http-client): HTTP client for Clojure and babashka built on java.net.http +## Unreleased + +- [#80](https://github.com/babashka/http-client/issues/80): accept a function of the request URI in `:proxy` to select a proxy per request ([@jeeger](https://github.com/jeeger)) + ## 0.4.23 (2025-06-06) - [#75](https://github.com/babashka/http-client/issues/75): override existing content type header in multipart request diff --git a/src/babashka/http_client.clj b/src/babashka/http_client.clj index 36dbb84..27c0a94 100644 --- a/src/babashka/http_client.clj +++ b/src/babashka/http_client.clj @@ -7,12 +7,19 @@ i/default-client-opts) (defn ->ProxySelector - "Constructs a `java.net.ProxySelector`. + "Constructs a `java.net.ProxySelector` from a map of options, or from a + function of a `java.net.URI` returning such a map to select a proxy per + request. The function may return `nil` to connect directly. + Options: * `:host` - string - * `:port` - long" - [opts] - (i/->ProxySelector opts)) + * `:port` - long + * `:type` - `:http` (default) or `:direct`, which ignores `:host` and `:port`. + `java.net.http` connects through HTTP proxies only." + [opts-or-fn] + (if (instance? java.net.ProxySelector opts-or-fn) + opts-or-fn + (i/->ProxySelector opts-or-fn))) (defn ->SSLContext "Constructs a `javax.net.ssl.SSLContext`. diff --git a/src/babashka/http_client/internal.clj b/src/babashka/http_client/internal.clj index dea6807..b7aa123 100644 --- a/src/babashka/http_client/internal.clj +++ b/src/babashka/http_client/internal.clj @@ -97,13 +97,43 @@ trust-managers (SecureRandom.)))))) -(defn ->ProxySelector +(defn- ->Proxy [opts] - (if (instance? java.net.ProxySelector opts) + (if (instance? java.net.Proxy opts) opts - (let [{:keys [host port]} opts] - (cond (and host port) - (java.net.ProxySelector/of (java.net.InetSocketAddress. ^String host ^long port)))))) + (let [{:keys [host port type] :or {type :http}} opts] + (case type + :direct java.net.Proxy/NO_PROXY + :http (if (and host port) + (java.net.Proxy. java.net.Proxy$Type/HTTP + (java.net.InetSocketAddress. ^String host ^long port)) + (throw (ex-info "Proxy needs both :host and :port." {:opts opts}))) + (throw (ex-info (str "Unsupported proxy type: " (pr-str type) + ". java.net.http connects through HTTP proxies only.") + {:opts opts})))))) + +(defn ->ProxySelector + [opts-or-fn] + (cond + (instance? java.net.ProxySelector opts-or-fn) + opts-or-fn + (fn? opts-or-fn) + ;; Create a dynamic proxy selector. + (proxy [java.net.ProxySelector] [] + (connectFailed [_ _ _]) + (select [^URI uri] + ;; Only allow the proxy function to return a single proxy. + ;; Returning multiple is currently not supported. + [(if-let [proxy-values (opts-or-fn uri)] + (->Proxy proxy-values) + java.net.Proxy/NO_PROXY)])) + :else + ;; Return a static proxy configuration that always returns the same proxy. + (let [static-proxy (->Proxy opts-or-fn)] + (proxy [java.net.ProxySelector] [] + (connectFailed [_ _ _]) + (select [^URI _uri] + [static-proxy]))))) (defn ->Authenticator [v] diff --git a/test/babashka/http_client/internal/helpers_test.clj b/test/babashka/http_client/internal/helpers_test.clj index ef03e82..6562c5b 100644 --- a/test/babashka/http_client/internal/helpers_test.clj +++ b/test/babashka/http_client/internal/helpers_test.clj @@ -4,5 +4,5 @@ [clojure.test :as t])) (t/deftest ->uri-tests - (let [uri (h/->uri {:scheme "https" :host "example.com" :path "/foo"})] + (let [^java.net.URI uri (h/->uri {:scheme "https" :host "example.com" :path "/foo"})] (t/is (= (.getPort uri) -1)))) diff --git a/test/babashka/http_client_test.clj b/test/babashka/http_client_test.clj index bb58a96..bee92a9 100644 --- a/test/babashka/http_client_test.clj +++ b/test/babashka/http_client_test.clj @@ -510,8 +510,44 @@ (deftest proxy-selector (is (instance? java.net.ProxySelector - (http/->ProxySelector {:host "https://clojure.org" - :port 1337})))) + (http/->ProxySelector {:host "clojure.org" + :port 1337}))) + (testing "a ProxySelector is passed through" + (is (instance? java.net.ProxySelector + (http/->ProxySelector + (http/->ProxySelector {:host "clojure.org" + :port 1337}))))) + (testing "a function selects a proxy per request" + (let [^java.net.ProxySelector selector + (http/->ProxySelector (fn [^java.net.URI uri] + (when (= (.getScheme uri) "http") + {:host "www.example.org" + :port 128}))) + proxies-for-http (.select selector (java.net.URI. "http://www.example.org")) + proxies-for-https (.select selector (java.net.URI. "https://www.example.org"))] + (is (= (count proxies-for-http) (count proxies-for-https) 1)) + (is (= (.type ^java.net.Proxy (get proxies-for-http 0)) java.net.Proxy$Type/HTTP)) + (is (= (.address ^java.net.Proxy (get proxies-for-http 0)) + (java.net.InetSocketAddress. "www.example.org" 128))) + (testing "returning nil connects directly" + (is (= (.type ^java.net.Proxy (get proxies-for-https 0)) java.net.Proxy$Type/DIRECT))))) + (testing "a map always selects the same proxy" + (let [^java.net.ProxySelector selector (http/->ProxySelector {:host "127.0.0.1" + :port 8081}) + selected (.select selector (java.net.URI. "http://www.example.org"))] + (is (= (count selected) 1)) + (is (= (.type ^java.net.Proxy (selected 0)) java.net.Proxy$Type/HTTP)) + (is (= (.address ^java.net.Proxy (selected 0)) (java.net.InetSocketAddress. "127.0.0.1" 8081))))) + (testing ":direct ignores host and port" + (let [^java.net.ProxySelector selector (http/->ProxySelector {:type :direct})] + (is (= (.type ^java.net.Proxy ((.select selector (java.net.URI. "http://www.example.org")) 0)) + java.net.Proxy$Type/DIRECT)))) + (testing "java.net.http supports HTTP proxies only" + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Unsupported proxy type" + (http/->ProxySelector {:host "127.0.0.1" :port 8081 :type :socks})))) + (testing "a proxy needs host and port" + (is (thrown-with-msg? clojure.lang.ExceptionInfo #":host and :port" + (http/->ProxySelector {:host "127.0.0.1"}))))) (deftest cookie-handler-test (testing "nil passthrough"