[#979] JDK 11 Http Client

Co-authored-by: Michael Glaesemann <grzm@seespotcode.net>
This commit is contained in:
Michiel Borkent 2021-08-31 11:13:11 +02:00 committed by GitHub
parent cebdd19c00
commit b71278cc68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 559 additions and 2 deletions

View file

@ -158,12 +158,19 @@
java.math.BigInteger java.math.BigInteger
java.math.MathContext java.math.MathContext
java.math.RoundingMode java.math.RoundingMode
java.net.Authenticator
java.net.ConnectException java.net.ConnectException
java.net.CookieHandler
java.net.CookieManager
java.net.CookieStore
java.net.DatagramSocket java.net.DatagramSocket
java.net.DatagramPacket java.net.DatagramPacket
java.net.HttpCookie
java.net.HttpURLConnection java.net.HttpURLConnection
java.net.InetAddress java.net.InetAddress
java.net.InetSocketAddress java.net.InetSocketAddress
java.net.PasswordAuthentication
java.net.ProxySelector
java.net.ServerSocket java.net.ServerSocket
java.net.Socket java.net.Socket
java.net.SocketException java.net.SocketException
@ -172,6 +179,29 @@
;; java.net.URL, see below ;; java.net.URL, see below
java.net.URLEncoder java.net.URLEncoder
java.net.URLDecoder java.net.URLDecoder
;; java.net.http
jdk.internal.net.http.HttpClientBuilderImpl
jdk.internal.net.http.HttpClientFacade
jdk.internal.net.http.HttpRequestBuilderImpl
jdk.internal.net.http.HttpResponseImpl
jdk.internal.net.http.common.MinimalFuture
jdk.internal.net.http.websocket.BuilderImpl
jdk.internal.net.http.websocket.WebSocketImpl
java.net.http.HttpClient
java.net.http.HttpClient$Builder
java.net.http.HttpClient$Redirect
java.net.http.HttpClient$Version
java.net.http.HttpHeaders
java.net.http.HttpRequest
java.net.http.HttpRequest$BodyPublisher
java.net.http.HttpRequest$BodyPublishers
java.net.http.HttpRequest$Builder
java.net.http.HttpResponse
java.net.http.HttpResponse$BodyHandler
java.net.http.HttpResponse$BodyHandlers
java.net.http.WebSocket
java.net.http.WebSocket$Builder
java.net.http.WebSocket$Listener
~@(when features/java-nio? ~@(when features/java-nio?
'[java.nio.ByteBuffer '[java.nio.ByteBuffer
java.nio.ByteOrder java.nio.ByteOrder
@ -180,6 +210,7 @@
java.nio.DirectByteBufferR java.nio.DirectByteBufferR
java.nio.MappedByteBuffer java.nio.MappedByteBuffer
java.nio.file.OpenOption java.nio.file.OpenOption
java.nio.file.StandardOpenOption
java.nio.channels.FileChannel java.nio.channels.FileChannel
java.nio.channels.FileChannel$MapMode java.nio.channels.FileChannel$MapMode
java.nio.charset.Charset java.nio.charset.Charset
@ -269,7 +300,11 @@
java.util.Properties java.util.Properties
java.util.Set java.util.Set
java.util.UUID java.util.UUID
java.util.concurrent.CompletableFuture
java.util.concurrent.Executors
java.util.concurrent.TimeUnit java.util.concurrent.TimeUnit
java.util.function.Function
java.util.function.Supplier
java.util.zip.InflaterInputStream java.util.zip.InflaterInputStream
java.util.zip.DeflaterInputStream java.util.zip.DeflaterInputStream
java.util.zip.GZIPInputStream java.util.zip.GZIPInputStream
@ -277,6 +312,8 @@
java.util.zip.ZipInputStream java.util.zip.ZipInputStream
java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream
java.util.zip.ZipEntry java.util.zip.ZipEntry
javax.net.ssl.SSLContext
javax.net.ssl.SSLParameters
~(symbol "[B") ~(symbol "[B")
~(symbol "[I") ~(symbol "[I")
~(symbol "[Ljava.lang.Object;") ~(symbol "[Ljava.lang.Object;")
@ -289,6 +326,10 @@
:methods [borkdude.graal.LockFix] ;; support for locking :methods [borkdude.graal.LockFix] ;; support for locking
:fields [clojure.lang.PersistentQueue] :fields [clojure.lang.PersistentQueue]
;; this just adds the class without any methods also suitable for private
;; classes: add the privage class here and the public class to the normal
;; list above and then everything reachable via the public class will be
;; visible in the native image.
:instance-checks [clojure.lang.AMapEntry ;; for proxy :instance-checks [clojure.lang.AMapEntry ;; for proxy
clojure.lang.APersistentMap ;; for proxy clojure.lang.APersistentMap ;; for proxy
clojure.lang.AReference clojure.lang.AReference
@ -397,7 +438,9 @@
(instance? java.nio.CharBuffer v) (instance? java.nio.CharBuffer v)
java.nio.CharBuffer java.nio.CharBuffer
(instance? java.nio.channels.FileChannel v) (instance? java.nio.channels.FileChannel v)
java.nio.channels.FileChannel))))) java.nio.channels.FileChannel
(instance? java.net.CookieStore v)
java.net.CookieStore)))))
(def class-map (gen-class-map)) (def class-map (gen-class-map))

View file

@ -57,4 +57,13 @@
(key [] ((method-or-bust methods 'key) this)) (key [] ((method-or-bust methods 'key) this))
(val [] ((method-or-bust methods 'val) this)) (val [] ((method-or-bust methods 'val) this))
(getKey [] ((method-or-bust methods 'getKey) this)) (getKey [] ((method-or-bust methods 'getKey) this))
(getValue [] ((method-or-bust methods 'getValue) this)))))) (getValue [] ((method-or-bust methods 'getValue) this)))
["java.net.Authenticator" #{}]
(proxy [java.net.Authenticator] []
(getPasswordAuthentication [] ((method-or-bust methods 'getPasswordAuthentication) this)))
["java.net.ProxySelector" #{}]
(proxy [java.net.ProxySelector] []
(connectFailed [_ _ _] ((method-or-bust methods 'connectFailed) this))
(select [_ _] ((method-or-bust methods 'select) this))))))

View file

@ -148,9 +148,24 @@
{iterator [[this]] {iterator [[this]]
forEach [[this action]]} forEach [[this action]]}
java.net.http.WebSocket$Listener
{onBinary [[this ws data last?]]
onClose [[this ws status-code reason]]
onError [[this ws error]]
onOpen [[this ws]]
onPing [[this ws data]]
onPong [[this ws data]]
onText [[this ws data last?]]}
java.util.Iterator java.util.Iterator
{hasNext [[this]] {hasNext [[this]]
next [[this]]} next [[this]]}
java.util.function.Function
{apply [[this t]]}
java.util.function.Supplier
{get [[this]]}
java.lang.Comparable java.lang.Comparable
{compareTo [[this other]]}})) {compareTo [[this other]]}}))

View file

@ -0,0 +1,490 @@
(ns babashka.java-http-client-test
(:require
[babashka.test-utils :as test-utils]
[clojure.edn :as edn]
[clojure.string :as str]
[clojure.test :as test :refer [deftest is]]
[org.httpkit.server :as httpkit.server]))
(defn bb [expr]
(edn/read-string (apply test-utils/bb nil [(str expr)])))
(deftest java-http-client-test
(is (= [200 true]
(bb
'(do (ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)))
(def req
(-> (HttpRequest/newBuilder (URI. "https://www.clojure.org"))
(.GET)
(.build)))
(def client
(-> (HttpClient/newBuilder)
(.build)))
(def resp (.send client req (HttpResponse$BodyHandlers/ofString)))
[(.statusCode resp) (string? (.body resp))])))))
(deftest redirect-test
(let [redirect-prog
(fn [redirect-kind]
(str/replace (str '(do
(ns net
(:import
(java.net.http HttpClient
HttpClient$Redirect
HttpRequest
HttpRequest$BodyPublishers
HttpResponse$BodyHandlers)
(java.net URI)))
(defn log [x] (.println System/err x))
(let [req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com"))
(.GET)
(.timeout (java.time.Duration/ofSeconds 5))
(.build))
client (-> (HttpClient/newBuilder)
(.followRedirects :redirect/kind)
(.build))
handler (HttpResponse$BodyHandlers/discarding)]
(.statusCode (.send client req handler)))))
":redirect/kind"
(case redirect-kind
:never
"HttpClient$Redirect/NEVER"
:always
"HttpClient$Redirect/ALWAYS")))]
;; TODO: make graalvm repro of never-ending request with redirect always on linux aarch64 (+ musl?)
(when-not (and (= "aarch64" (System/getenv "BABASHKA_ARCH"))
(= "linux" (System/getenv "BABASHKA_PLATFORM")))
(println "Testing redirect always")
(is (= 200 (bb (redirect-prog :always)))))
(println "Testing redirect never")
(is (= 302 (bb (redirect-prog :never))))))
(deftest connect-timeout-test
(is (= "java.net.http.HttpConnectTimeoutException"
(bb
'(do
(ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(java.time Duration)))
(let [client (-> (HttpClient/newBuilder)
(.connectTimeout (Duration/ofMillis 1))
(.build))
req (-> (HttpRequest/newBuilder (URI. "Https://www.postman-echo.com/get"))
(.GET)
(.build))]
(try
(.send client req (HttpResponse$BodyHandlers/discarding))
(catch Throwable t
(-> (Throwable->map t)
:via
first
:type
name)))))))))
(deftest executor
(is (= 200
(bb
'(do
(ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(java.util.concurrent Executors)))
(let [uri (URI. "https://www.postman-echo.com/get")
req (-> (HttpRequest/newBuilder uri)
(.GET)
(.build))
client (-> (HttpClient/newBuilder)
(.executor (Executors/newSingleThreadExecutor))
(.build))
res (.send client req (HttpResponse$BodyHandlers/discarding))]
(.statusCode res)))))))
(deftest client-proxy
(is (= true
(bb
'(do
(ns net
(:import
(java.net ProxySelector)
(java.net.http HttpClient)))
(let [bespoke-proxy (proxy [ProxySelector] []
(connectFailed [_ _ _])
(select [_ _]))
client (-> (HttpClient/newBuilder)
(.proxy bespoke-proxy)
(.build))]
(= bespoke-proxy (-> (.proxy client)
(.get))))))))
(is (= 200
(bb
'(do
(ns net
(:import
(java.net ProxySelector
URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)))
(let [uri (URI. "https://www.postman-echo.com/get")
req (-> (HttpRequest/newBuilder uri)
(.build))
client (-> (HttpClient/newBuilder)
(.proxy (ProxySelector/getDefault))
(.build))
res (.send client req (HttpResponse$BodyHandlers/discarding))]
(.statusCode res)))))))
(deftest ssl-test
(is (= 200
(bb
'(do
(ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(javax.net.ssl SSLContext
SSLParameters)))
(let [uri (URI. "https://www.postman-echo.com/get")
req (-> (HttpRequest/newBuilder uri)
(.build))
ssl-context (doto (SSLContext/getInstance "TLS")
(.init nil nil nil))
client (-> (HttpClient/newBuilder)
(.sslContext ssl-context)
(.build))
res (.send client req (HttpResponse$BodyHandlers/discarding))]
(.statusCode res)))))))
(deftest send-async-test
(is (= 200
(bb
'(do
(ns net
(:import
(java.net ProxySelector
URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(java.time Duration)
(java.util.function Function)))
(let [client (-> (HttpClient/newBuilder)
(.build))
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/get"))
(.GET)
(.build))]
(-> (.sendAsync client req (HttpResponse$BodyHandlers/discarding))
(.thenApply (reify Function (apply [_ t] (.statusCode t))))
(deref))))))))
(deftest body-publishers-test
(is (= true
(bb
'(do
(ns net
(:require
[cheshire.core :as json]
[clojure.java.io :as io]
[clojure.string :as str])
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpRequest$BodyPublishers
HttpResponse$BodyHandlers)
(java.util.function Supplier)))
(let [bp (HttpRequest$BodyPublishers/ofFile (.toPath (io/file "README.md")))
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/post"))
(.method "POST" bp)
(.build))
client (-> (HttpClient/newBuilder)
(.build))
res (.send client req (HttpResponse$BodyHandlers/ofString))
body-data (-> (.body res) (json/parse-string true) :data)]
(str/includes? body-data "babashka"))))))
(let [body "with love from java.net.http"]
(is (= {:of-input-stream body
:of-byte-array body
:of-byte-arrays body}
(bb
'(do
(ns net
(:require
[cheshire.core :as json]
[clojure.java.io :as io])
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpRequest$BodyPublishers
HttpResponse$BodyHandlers)
(java.util.function Supplier)))
(let [body "with love from java.net.http"
publishers {:of-input-stream (HttpRequest$BodyPublishers/ofInputStream
(reify Supplier (get [_] (io/input-stream (.getBytes body)))))
:of-byte-array (HttpRequest$BodyPublishers/ofByteArray (.getBytes body))
:of-byte-arrays (HttpRequest$BodyPublishers/ofByteArrays [(.getBytes body)])}
client (-> (HttpClient/newBuilder)
(.build))
body-data (fn [res] (-> (.body res) (json/parse-string true) :data))]
(->> publishers
(map (fn [[k body-publisher]]
(let [req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/post"))
(.method "POST" body-publisher)
(.build))]
[k (-> (.send client req (HttpResponse$BodyHandlers/ofString))
(body-data))])))
(into {}))))))))
(when-not test-utils/windows?
;; TODO: somehow doesn't work in Windows, should it?
(let [body "おはようございます!"]
(is (= body
(bb
'(do
(ns net
(:require
[cheshire.core :as json]
[clojure.java.io :as io])
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpRequest$BodyPublishers
HttpResponse$BodyHandlers)
(java.nio.charset Charset)
(java.util.function Supplier)))
(let [body "おはようございます!"
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/post"))
(.method "POST" (HttpRequest$BodyPublishers/ofString
body (Charset/forName "UTF-16")))
(.header "Content-Type" "text/plain; charset=utf-16")
(.build))
client (-> (HttpClient/newBuilder)
(.build))
res (.send client req (HttpResponse$BodyHandlers/ofString))]
(-> (.body res)
(json/parse-string true)
:data)))))))))
(deftest cookie-test
(is (= []
(bb '(do (ns net
(:import [java.net CookieManager]))
(-> (CookieManager.)
(.getCookieStore)
(.getCookies))))))
(is (= "www.postman-echo.com"
(bb '(do
(ns net
(:import
(java.net CookieManager
URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)))
(let [client (-> (HttpClient/newBuilder)
(.cookieHandler (CookieManager.))
(.build))
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/get"))
(.GET)
(.build))]
(.send client req (HttpResponse$BodyHandlers/discarding))
(-> client
(.cookieHandler)
(.get)
(.getCookieStore)
(.getCookies)
first
(.getDomain))))))))
(deftest authenticator-test
(is (= [401 200]
(bb
'(do
(ns net
(:import
(java.net Authenticator
PasswordAuthentication
URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)))
(let [no-auth-client (-> (HttpClient/newBuilder)
(.build))
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/basic-auth"))
(.build))
handler (HttpResponse$BodyHandlers/discarding)
no-auth-res (.send no-auth-client req handler)
authenticator (proxy [Authenticator] []
(getPasswordAuthentication []
(PasswordAuthentication. "postman" (char-array "password"))))
auth-client (-> (HttpClient/newBuilder)
(.authenticator authenticator)
(.build))
auth-res (.send auth-client req handler)]
[(.statusCode no-auth-res) (.statusCode auth-res)]))))))
(deftest cert-test
;; TODO: investigate aarch64 issue
(when-not
(and (= "aarch64" (System/getenv "BABASHKA_ARCH"))
(= "linux" (System/getenv "BABASHKA_PLATFORM")))
(is (= {:expired "java.security.cert.CertificateExpiredException"
:revoked 200 ;; TODO: fix, "sun.security.cert.CertificateRevokedException"
:self-signed "sun.security.provider.certpath.SunCertPathBuilderException"
:untrusted-root "sun.security.provider.certpath.SunCertPathBuilderException"
:wrong-host "sun.security.provider.certpath.SunCertPathBuilderException"}
(bb
'(do
(ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)))
(defn send-and-catch [client req handler]
(try
(let [res (.send client req (HttpResponse$BodyHandlers/discarding))]
(.statusCode res))
(catch Throwable t
(-> (Throwable->map t) :via last :type name))))
(let [client (-> (HttpClient/newBuilder)
(.build))
handler (HttpResponse$BodyHandlers/discarding)
reqs (->> [:expired
:self-signed
:revoked
:untrusted-root
:wrong-host]
(map (fn [k]
(let [req (-> (URI. (format "https://%s.badssl.com" (name k)))
(HttpRequest/newBuilder)
(.GET)
(.build))]
[k req])))
(into {}))]
(->> reqs
(map (fn [[k req]]
[k (send-and-catch client req handler)]))
(into {})))))))))
(deftest request-timeout-test
(is (= "java.net.http.HttpTimeoutException"
(bb
'(do
(ns net
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(java.time Duration)))
(let [client (-> (HttpClient/newBuilder)
(.build))
req (-> (HttpRequest/newBuilder (URI. "https://www.postman-echo.com/delay/1"))
(.GET)
(.timeout (Duration/ofMillis 200))
(.build))]
(try
(.send client req (HttpResponse$BodyHandlers/discarding))
(catch Throwable t
(-> (Throwable->map t)
:via
first
:type
name)))))))))
(deftest body-handlers-test
(is (= true
(bb
'(do
(ns net
(:require
[clojure.string :as str])
(:import
(java.net URI)
(java.net.http HttpClient
HttpRequest
HttpResponse$BodyHandlers)
(java.nio.file Files StandardOpenOption)
(java.nio.file.attribute FileAttribute)))
(let [client (-> (HttpClient/newBuilder)
(.build))
uri (URI. "https://raw.githubusercontent.com/babashka/babashka/master/README.md")
req (-> (HttpRequest/newBuilder uri)
(.GET)
(.build))
temp-file (Files/createTempFile "bb-prefix-" "-bb-suffix" (make-array FileAttribute 0))
open-options (into-array StandardOpenOption [StandardOpenOption/CREATE
StandardOpenOption/WRITE])
handler (HttpResponse$BodyHandlers/ofFile temp-file open-options)
res (.send client req handler)
temp-file-path (str (.body res))
contents (slurp temp-file-path)]
(str/includes? contents "babashka")))))))
(defn ws-handler [{:keys [init] :as opts} req]
(when init (init req))
(httpkit.server/as-channel
req
(select-keys opts [:on-close :on-ping :on-receive])))
(def ^:dynamic *ws-port* 1234)
(defmacro with-ws-server
[opts & body]
`(let [s# (httpkit.server/run-server (partial ws-handler ~opts) {:port ~*ws-port*})]
(try ~@body (finally (s# :timeout 100)))))
(deftest websockets-test
(with-ws-server {:on-receive #(httpkit.server/send! %1 %2)}
(is (= "zomg websockets!"
(bb
'(do
(ns net
(:require
[clojure.string :as str])
(:import
(java.net URI)
(java.net.http HttpClient
WebSocket$Listener)
(java.util.concurrent CompletableFuture)
(java.util.function Function)))
(let [p (promise)
uri (URI. "ws://localhost:1234")
listener (reify WebSocket$Listener
(onOpen [_ ws]
(.request ws 1))
(onText [_ ws data last?]
(.request ws 1)
(.thenApply (CompletableFuture/completedFuture nil)
(reify Function
(apply [_ _] (deliver p (str data)))))))
client (HttpClient/newHttpClient)
ws (-> (.newWebSocketBuilder client)
(.buildAsync uri listener)
(deref))]
(.sendText ws "zomg websockets!" true)
(deref p 5000 ::timeout))))))))