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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions src/babashka/http_client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
40 changes: 35 additions & 5 deletions src/babashka/http_client/internal.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion test/babashka/http_client/internal/helpers_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
40 changes: 38 additions & 2 deletions test/babashka/http_client_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading