diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d729c7..28ff510 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): add idiomatic proxy configuration functionality + ## 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..53b5380 100644 --- a/src/babashka/http_client.clj +++ b/src/babashka/http_client.clj @@ -7,12 +7,30 @@ i/default-client-opts) (defn ->ProxySelector - "Constructs a `java.net.ProxySelector`. + "Constructs a `java.net.ProxySelector`. Can either take a map of `:host` and `:port` actions, or a proxy + creation function that takes a java.net.URI and returns a proxy configuration for ->Proxy. + + Options: + * `:host` - string + * `:port` - long + Proxy function: + * `fn`: A one-argument function receiving a java.net.URI argument and returning a proxy configuration for ->Proxy or nil + (meaning don't use a proxy)." + [opts-or-fn] + (cond + (instance? java.net.ProxySelector opts-or-fn) opts-or-fn + (map? opts-or-fn) (i/->ProxySelector opts-or-fn) + :else (i/fn->ProxySelector opts-or-fn))) + +(defn ->Proxy + "Constructs a `java.net.Proxy`. + Options: * `:host` - string - * `:port` - long" + * `:port` - long + * `:type` - One of `:direct` (no proxy), `:socks` (socks proxy) or `:http` (application level proxy)." [opts] - (i/->ProxySelector opts)) + (i/->Proxy opts)) (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..889921b 100644 --- a/src/babashka/http_client/internal.clj +++ b/src/babashka/http_client/internal.clj @@ -36,6 +36,12 @@ :never HttpClient$Redirect/NEVER :normal HttpClient$Redirect/NORMAL)) +(defn- ->proxy-type [type] + (case type + :direct java.net.Proxy$Type/DIRECT + :socks java.net.Proxy$Type/SOCKS + :http java.net.Proxy$Type/HTTP)) + (defn- version-keyword->version-enum [version] (case version :http1.1 HttpClient$Version/HTTP_1_1 @@ -105,6 +111,27 @@ (cond (and host port) (java.net.ProxySelector/of (java.net.InetSocketAddress. ^String host ^long port)))))) +(defn ->Proxy + [opts] + (if (instance? java.net.Proxy opts) + opts + (let [{:keys [host port type]} opts] + (cond + (= type :direct) + java.net.Proxy/NO_PROXY + (and host port type) + (java.net.Proxy. (->proxy-type type) (java.net.InetSocketAddress. ^String host ^long port)))))) + +(defn fn->ProxySelector [proxy-fn] + (proxy [java.net.ProxySelector] [] + (connectFailed [_ _ _]) + (select [^URI uri] + ;; Only allow the proxy function to return a single proxy. + ;; I don't really see the use case for multiple. + [(if-let [proxy-opts (proxy-fn uri)] + (->Proxy proxy-opts) + java.net.Proxy/NO_PROXY)]))) + (defn ->Authenticator [v] (if (instance? 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 9a6a924..1807486 100644 --- a/test/babashka/http_client_test.clj +++ b/test/babashka/http_client_test.clj @@ -3,6 +3,7 @@ [babashka.fs :as fs] [babashka.http-client :as http] [babashka.http-client.interceptors :as i] + [babashka.http-client.internal :as internal] [babashka.http-client.internal.version :as iv] [borkdude.deflet :refer [deflet]] [cheshire.core :as json] @@ -473,7 +474,23 @@ (deftest proxy-selector (is (instance? java.net.ProxySelector (http/->ProxySelector {:host "https://clojure.org" - :port 1337})))) + :port 1337}))) + ;; Check passthrough behavior. + (is (instance? java.net.ProxySelector + (http/->ProxySelector + (http/->ProxySelector {:host "https://clojure.org" + :port 1337})))) + (let [^java.net.ProxySelector complex-proxy-selector (http/->ProxySelector (fn [^java.net.URI uri] + (when (= (.getScheme uri) "http") + {:host "http://www.example.org" + :port 128 + :type :http})))] + (is (instance? java.net.ProxySelector complex-proxy-selector)) + (let [proxies-for-http (.select complex-proxy-selector (java.net.URI. "http://www.example.org")) + proxies-for-https (.select complex-proxy-selector (java.net.URI. "https://www.example.org"))] + (is (= (count proxies-for-http) (count proxies-for-https) 1)) + (is (= (.type (get proxies-for-http 0)) java.net.Proxy$Type/HTTP)) + (is (= (.type (get proxies-for-https 0)) java.net.Proxy$Type/DIRECT))))) (deftest cookie-handler-test (testing "nil passthrough"