├── resources └── jetty-logging.properties ├── .gitignore ├── tests.edn ├── env └── dev │ └── clj │ └── user.clj ├── src └── java_http_clj │ ├── util.clj │ ├── specs.clj │ ├── websocket.clj │ └── core.clj ├── .circleci └── config.yml ├── CHANGELOG.md ├── LICENSE ├── project.clj ├── test └── java_http_clj │ ├── test_server.clj │ ├── integration_test.clj │ ├── core_test.clj │ └── websocket_test.clj └── README.md /resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StrErrLog 2 | org.eclipse.jetty.LEVEL=WARN 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /codox 13 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | {:kaocha/tests [{:kaocha.testable/type :kaocha.type/clojure.test 2 | :kaocha.testable/id :unit 3 | :kaocha/ns-patterns ["-test$"] 4 | :kaocha/source-paths ["src"] 5 | :kaocha/test-paths ["test/java_http_clj"]}] 6 | :kaocha/fail-fast? false 7 | :kaocha/color? true 8 | :kaocha/reporter [kaocha.report/documentation]} 9 | -------------------------------------------------------------------------------- /env/dev/clj/user.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc user 2 | (:require [clojure.spec.test.alpha :as st] 3 | [clojure.tools.namespace.repl :as tn] 4 | [java-http-clj.websocket-test] 5 | [mount.core :as mount])) 6 | 7 | (defn refresh [] 8 | (let [r (tn/refresh)] 9 | (st/instrument) 10 | r)) 11 | 12 | (defn restart [] 13 | (mount/stop) 14 | (refresh) 15 | (mount/start-without #'java-http-clj.websocket-test/ws)) 16 | -------------------------------------------------------------------------------- /src/java_http_clj/util.clj: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc java-http-clj.util 2 | (:import [java.time Duration] 3 | [java.util.function Function])) 4 | 5 | (set! *warn-on-reflection* true) 6 | 7 | (defmacro add-docstring [var docstring] 8 | `(alter-meta! ~var #(assoc % :doc ~docstring))) 9 | 10 | (defn convert-timeout [t] 11 | (if (integer? t) 12 | (Duration/ofMillis t) 13 | t)) 14 | 15 | (defmacro clj-fn->function ^Function [f] 16 | `(reify Function 17 | (~'apply [_# x#] (~f x#)))) 18 | 19 | (def shorthands [:get :head :post :put :delete]) 20 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/clojure:openjdk-11-lein-2.8.3 6 | 7 | working_directory: ~/repo 8 | 9 | environment: 10 | LEIN_ROOT: "true" 11 | 12 | steps: 13 | - checkout 14 | 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "project.clj" }} 18 | - v1-dependencies- 19 | 20 | - run: lein with-profile +kaocha deps 21 | 22 | - save_cache: 23 | paths: 24 | - ~/.m2 25 | key: v1-dependencies-{{ checksum "project.clj" }} 26 | 27 | - run: lein kaocha 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.3 2 | 3 | - Move specs from `java-http-clj.core` and `java-http-clj.websocket` to `java-http-clj.specs` to make java-http-clj easier to use with [Babashka](https://github.com/babashka/babashka/). 4 | 5 | ## 0.4.2 6 | 7 | - Use `ifn?` for callbacks instead of `fn?` 8 | 9 | ## 0.4.1 10 | 11 | - Add type hints to functions that return Java objects 12 | - Set Clojure dependency as `:scope "provided"` 13 | - Small bugfixes for WebSocket and specs 14 | 15 | ## 0.4.0 16 | 17 | - Add WebSocket API 18 | 19 | ## 0.3.1 20 | 21 | - Add specs for all API functions 22 | 23 | ## 0.3.0 24 | 25 | - Fix NPE for requests without bodies 26 | 27 | ## 0.2.0 28 | 29 | - Rename a few methods 30 | - `make-client` -> `build-client` 31 | - `make-request` -> `build-request` 32 | - `resp->ring` -> `response->map` 33 | 34 | ## 0.1.0 35 | 36 | - Initial release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 John Schmidt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject java-http-clj "0.4.3" 2 | :description "A lightweight Clojure wrapper for java.net.http" 3 | :url "http://www.github.com/schmee/java-http-clj" 4 | :license {:name "MIT" 5 | :url "https://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.10.3" :scope "provided"]] 7 | :profiles {:dev {:dependencies [[com.cemerick/url "0.1.1"] 8 | [compojure "1.6.2"] 9 | [info.sunng/ring-jetty9-adapter "0.14.2"] 10 | [mount "0.1.16"] 11 | [org.clojure/tools.namespace "1.1.0"] 12 | [pjstadig/humane-test-output "0.11.0"] 13 | [ring "1.9.4"] 14 | [ring/ring-defaults "0.3.3"]] 15 | :source-paths ["src" "env/dev/clj" "test"] 16 | :injections [(require 'pjstadig.humane-test-output) 17 | (pjstadig.humane-test-output/activate!)]} 18 | :kaocha {:dependencies [[lambdaisland/kaocha "1.0.861"]]}} 19 | :repl-options {:init-ns java-http-clj.core} 20 | :aliases {"kaocha" ["with-profile" "+kaocha" "run" "-m" "kaocha.runner"]}) 21 | -------------------------------------------------------------------------------- /test/java_http_clj/test_server.clj: -------------------------------------------------------------------------------- 1 | (ns ^:skip-test java-http-clj.test-server 2 | (:require [clojure.java.io :as io] 3 | [clojure.test :refer :all] 4 | [compojure.core :refer :all] 5 | [compojure.route :as route] 6 | [mount.core :as mount] 7 | [ring.adapter.jetty9 :as jetty] 8 | [ring.middleware.defaults :refer :all])) 9 | 10 | (defroutes app 11 | (GET "/" [] "ROOT") 12 | (GET "/echo" [message] (or message "no message")) 13 | (POST "/echo" r (slurp (io/reader (:body r)))) 14 | (PUT "/echo" r (slurp (io/reader (:body r)))) 15 | (DELETE "/echo" [] "deleted") 16 | (GET "/redir" [] {:status 302 :headers {"Location" "/target"}}) 17 | (GET "/target" [] "did redirect")) 18 | 19 | (def ws-handler {:on-connect (fn [ws]) 20 | :on-error (fn [ws e]) 21 | :on-close (fn [ws status-code reason]) 22 | :on-text 23 | (fn [ws text-message] 24 | (jetty/send! ws (str "SERVER: " text-message))) 25 | :on-bytes 26 | (fn [ws bs offset len] 27 | (jetty/send! ws bs))}) 28 | 29 | (mount/defstate server 30 | :start 31 | (let [{:keys [port] :or {port 8080}} (mount/args)] 32 | (jetty/run-jetty 33 | (wrap-defaults app api-defaults) 34 | {:port port 35 | :websockets {"/ws" ws-handler} 36 | :join? false})) 37 | :stop (.stop server)) 38 | -------------------------------------------------------------------------------- /test/java_http_clj/integration_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:integration java-http-clj.integration-test 2 | (:refer-clojure :exclude [send get]) 3 | (:require [cemerick.url :refer [url]] 4 | [clojure.java.io :as io] 5 | [clojure.test :refer :all] 6 | [clojure.spec.test.alpha :as st] 7 | [java-http-clj.core :refer :all] 8 | [java-http-clj.specs] 9 | [mount.core :as mount]) 10 | (:import [java.net.http HttpResponse] 11 | [java.util Arrays] 12 | [java.util.concurrent CompletableFuture])) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (st/instrument) 17 | 18 | (def port 8787) 19 | 20 | (defn wrap-setup [f] 21 | (mount/start (mount/with-args {:port port})) 22 | (f) 23 | (mount/stop)) 24 | 25 | (use-fixtures :once wrap-setup) 26 | 27 | (def base-url 28 | (assoc (url "http://localhost") :port port)) 29 | 30 | (defn make-url 31 | ([] 32 | (str base-url)) 33 | ([path] 34 | (str (url base-url path))) 35 | ([path params] 36 | (str (assoc (url base-url path) :query params)))) 37 | 38 | (def ^String s "some boring test string") 39 | 40 | (defn all-tests [f] 41 | (testing "request" 42 | (let [{:keys [body headers status version]} (send (make-url))] 43 | (is (= "ROOT" body)) 44 | ; (is (= ["content-length" "content-type" "date" "server"] (-> headers keys sort))) 45 | (is (= 200 status)) 46 | (is (= :http1.1 version)))) 47 | 48 | (testing "request-body-types" 49 | (let [send-and-get-body 50 | (fn [body] 51 | (:body (f {:uri (make-url "echo") 52 | :method :post 53 | :body body})))] 54 | (is (= "ROOT" (:body (send (make-url))))) 55 | (is (= s (send-and-get-body s))) 56 | (is (= s (send-and-get-body (.getBytes s)))) 57 | (is (= s (send-and-get-body (io/input-stream (.getBytes s))))))) 58 | 59 | (testing "response-body-types" 60 | (let [send-echo (fn [opts] (f (make-url "echo" {:message s}) opts))] 61 | (is (= s (:body (send-echo {:as :string})))) 62 | (is (Arrays/equals (.getBytes s) (:body (send-echo {:as :byte-array})))) 63 | (is (= s (-> (send-echo {:as :input-stream}) :body slurp))))) 64 | 65 | (testing "raw-opt" 66 | (is (instance? HttpResponse (f (make-url) {:raw? true})))) 67 | 68 | (testing "client-opt" 69 | ;; default client doesn't follow redirects 70 | (is (= 302 (:status (f (make-url "redir"))))) 71 | (let [client (build-client {:follow-redirects :always}) 72 | {:keys [body status]} (f (make-url "redir") {:client client})] 73 | (is (= 200 status)) 74 | (is (= "did redirect" body))))) 75 | 76 | (deftest test-send 77 | (all-tests send)) 78 | 79 | (deftest test-send-async 80 | (all-tests (comp deref send-async))) 81 | 82 | (deftest async-stuff 83 | (let [f (send-async (make-url)) 84 | r (.join f)] 85 | (is (instance? CompletableFuture f)) 86 | (is (map? r))) 87 | 88 | (testing "callback" 89 | (let [r (.join (send-async (make-url) {} #(assoc % :call :back) nil))] 90 | (is (= :back (:call r))))) 91 | 92 | (testing "ex-handler" 93 | (let [f (send-async 94 | (make-url) 95 | {} 96 | (fn [_] (throw (Exception. "oops!"))) 97 | (fn [_] :exception)) 98 | r (.join f)] 99 | (is (= :exception r))))) 100 | -------------------------------------------------------------------------------- /test/java_http_clj/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns java-http-clj.core-test 2 | (:refer-clojure :exclude [send get]) 3 | (:require [clojure.java.io :as io] 4 | [clojure.test :refer :all] 5 | [clojure.spec.test.alpha :as st] 6 | [java-http-clj.core :refer :all] 7 | [java-http-clj.specs]) 8 | (:import [java.net CookieManager ProxySelector URI] 9 | [java.net.http 10 | HttpClient$Redirect 11 | HttpClient$Version 12 | HttpHeaders 13 | HttpRequest$BodyPublisher 14 | HttpRequest$BodyPublishers 15 | HttpResponse] 16 | [java.time Duration] 17 | [java.util.concurrent Executors] 18 | [java.util.function BiPredicate] 19 | [javax.net.ssl SSLContext SSLParameters])) 20 | 21 | (set! *warn-on-reflection* true) 22 | 23 | (st/instrument) 24 | 25 | (defn build-fake-response [{:keys [status body version headers]}] 26 | (reify HttpResponse 27 | (body [this] body) 28 | (headers [this] headers) 29 | (statusCode [this] status) 30 | (version [this] version))) 31 | 32 | (def always-true-filter 33 | (reify BiPredicate 34 | (test [_ _ _] true))) 35 | 36 | (def fake-response 37 | (build-fake-response 38 | {:status 200 39 | :body "text, text everywhere" 40 | :version HttpClient$Version/HTTP_2 41 | :headers (HttpHeaders/of 42 | {"content-type" ["application/json"] 43 | "accept" ["deflate" "gzip"]} 44 | always-true-filter)})) 45 | 46 | (deftest response->map-test 47 | (let [{:keys [status body version headers]} (response->map fake-response)] 48 | (is (= 200 status)) 49 | (is (= :http2 version)) 50 | (is (= {"content-type" "application/json" 51 | "accept" ["deflate" "gzip"]} 52 | headers)) 53 | (is (= "text, text everywhere" body)))) 54 | 55 | (deftest build-request-test 56 | (let [req-map 57 | {:expect-continue? true 58 | :uri "http://www.google.com" 59 | :method :get 60 | :headers {"content-type" "application/json" 61 | "accept" ["deflate" "gzip"]} 62 | :timeout 2000 63 | :version :http2} 64 | r (build-request req-map)] 65 | (is (= true (.expectContinue r))) 66 | (is (= "http://www.google.com" (-> r .uri .toString))) 67 | (is (= "GET" (.method r))) 68 | (is (= {"content-type" ["application/json"] 69 | "accept" ["deflate" "gzip"]} 70 | (into {} (-> r .headers .map)))) 71 | (is (= (Duration/ofMillis 2000) (-> r .timeout .get))) 72 | (is (= (Duration/ofMillis 3000) 73 | (-> req-map (assoc :timeout (Duration/ofMillis 3000)) build-request .timeout .get))) 74 | (is (= HttpClient$Version/HTTP_2 (-> r .version .get))))) 75 | 76 | (deftest build-client-test 77 | (let [cookie-handler (CookieManager.) 78 | executor (Executors/newSingleThreadExecutor) 79 | proxy (proxy [ProxySelector] [] 80 | (connectFailed [_ _ _]) 81 | (select [_ _])) 82 | ssl-context (SSLContext/getInstance "TLS") 83 | ssl-parameters (SSLParameters. (into-array String ["TLS_DH_anon_WITH_AES_128_CBC_SHA"])) 84 | opts {:connect-timeout 2000 85 | :cookie-handler cookie-handler 86 | :executor executor 87 | :follow-redirects :always 88 | :priority 123 ;; There is no getter for priority, so good luck testing that 89 | :proxy proxy 90 | :ssl-context ssl-context 91 | :ssl-parameters ssl-parameters 92 | :version :http1.1} 93 | c (build-client opts)] 94 | (is (= (Duration/ofMillis 2000) (-> c .connectTimeout .get))) 95 | (is (= (Duration/ofMillis 3000) 96 | (-> opts (assoc :connect-timeout (Duration/ofMillis 3000)) build-client .connectTimeout .get))) 97 | (is (identical? cookie-handler (-> c .cookieHandler .get))) 98 | (is (identical? executor (-> c .executor .get))) 99 | (is (= HttpClient$Redirect/ALWAYS (-> c .followRedirects))) 100 | (is (identical? proxy (-> c .proxy .get))) 101 | (is (identical? ssl-context (-> c .sslContext))) 102 | (is (= ["TLS_DH_anon_WITH_AES_128_CBC_SHA"] (-> c .sslParameters .getCipherSuites vec))) 103 | (is (= HttpClient$Version/HTTP_1_1 (-> c .version))))) 104 | -------------------------------------------------------------------------------- /test/java_http_clj/websocket_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:integration java-http-clj.websocket-test 2 | (:refer-clojure :exclude [send]) 3 | (:require [clojure.java.io :as io] 4 | [clojure.test :refer :all] 5 | [clojure.spec.test.alpha :as st] 6 | [java-http-clj.specs] 7 | [java-http-clj.test-server] 8 | [java-http-clj.websocket :refer :all] 9 | [mount.core :as mount]) 10 | (:import [java.net.http 11 | HttpClient 12 | WebSocket 13 | WebSocket$Builder 14 | WebSocket$Listener] 15 | [java.nio ByteBuffer] 16 | [java.time Duration] 17 | [java.util Arrays])) 18 | 19 | (set! *warn-on-reflection* true) 20 | 21 | (st/instrument) 22 | 23 | (def port 8787) 24 | 25 | (defn received [] 26 | (zipmap 27 | [:on-binary :on-close :on-error :on-open :on-ping :on-pong :on-text] 28 | (repeatedly promise))) 29 | 30 | (def responses (atom (received))) 31 | 32 | (defn deliver-response [k v] 33 | (swap! responses update k deliver v)) 34 | 35 | (defn ^WebSocket build-a-websocket 36 | ([] (build-a-websocket {})) 37 | ([fns] 38 | (build-websocket 39 | "ws://localhost:8787/ws/" 40 | (merge 41 | {:on-binary (fn [_ data last?] (deliver-response :on-binary data)) 42 | :on-text (fn [ws data last?] (deliver-response :on-text data)) 43 | :on-error (fn [ws throwable] (deliver-response :on-error throwable)) 44 | :on-ping (fn [ws data] (deliver-response :on-ping data)) 45 | :on-pong (fn [ws data] (deliver-response :on-pong data)) 46 | :on-open (fn [ws] (deliver-response :on-open "did open")) 47 | :on-close (fn [ws status-code reason] (deliver-response :on-close [status-code reason]))} 48 | fns)))) 49 | 50 | (mount/defstate ^WebSocket ws 51 | :start (build-a-websocket) 52 | :stop (.abort ^WebSocket ws)) 53 | 54 | (use-fixtures :once 55 | (fn [f] 56 | (-> (mount/only [#'java-http-clj.test-server/server]) 57 | (mount/with-args {:port port}) 58 | mount/start) 59 | (f) 60 | (mount/stop))) 61 | 62 | (use-fixtures :each 63 | (fn [f] 64 | (reset! responses (received)) 65 | (mount/start #'java-http-clj.websocket-test/ws) 66 | (f) 67 | (mount/stop #'java-http-clj.websocket-test/ws))) 68 | 69 | (defn deref* [x] 70 | (deref x 100 ::timeout)) 71 | 72 | (deftest string 73 | (send ws "abc") 74 | (is (= "SERVER: abc" (-> @responses :on-text deref*)))) 75 | 76 | (deftest non-string 77 | (send ws 123) 78 | (is (= "SERVER: 123" (-> @responses :on-text deref*)))) 79 | 80 | (deftest some-bytes 81 | (send ws (byte-array [1 2 3])) 82 | (is (= [1 2 3] (-> @responses :on-binary deref* vec)))) 83 | 84 | (deftest byte-buffer 85 | (send ws (ByteBuffer/wrap (byte-array [4 5 6]))) 86 | (is (= [4 5 6] (-> @responses :on-binary deref* vec)))) 87 | 88 | (deftest pong-argument-conversion 89 | (.sendPing ws (ByteBuffer/wrap (byte-array [1 2 3]))) 90 | (is (Arrays/equals (byte-array [1 2 3]) (-> @responses :on-pong deref*)))) 91 | 92 | (deftest open-it 93 | (is (= "did open" (-> @responses :on-open deref*)))) 94 | 95 | (deftest errors 96 | (let [ws (build-a-websocket 97 | {:on-text (fn [_ _ _] (throw (Exception. "BOOM")))})] 98 | (send ws "abc") 99 | (let [e (-> @responses :on-error deref*)] 100 | (is (instance? Exception e)) 101 | (is (= "BOOM" (.getMessage e)))))) 102 | 103 | (deftest close-it 104 | (close ws) 105 | (is (.isOutputClosed ws)) 106 | (is (= [1000 ""] (-> @responses :on-close deref*)))) 107 | 108 | (deftest close-it-with-status-code 109 | (close ws 3333) 110 | (is (.isOutputClosed ws)) 111 | (is (= [3333 ""] (-> @responses :on-close deref*)))) 112 | 113 | (deftest close-it-with-status-code-and-reason 114 | (close ws 4444 "I'm out!") 115 | (is (.isOutputClosed ws)) 116 | (is (= [4444 "I'm out!"] (-> @responses :on-close deref*)))) 117 | 118 | (defn update-arg! [args k arg] 119 | (swap! args update k (fnil conj []) arg)) 120 | 121 | (defrecord FakeBuilder [args] 122 | WebSocket$Builder 123 | (header [this name value] 124 | (update-arg! args :header [name value]) this) 125 | (subprotocols [this most-preferred lesser-preferred] 126 | (update-arg! args :subprotocols [most-preferred lesser-preferred]) this) 127 | (connectTimeout [this timeout] 128 | (update-arg! args :connectTimeout [timeout]) this)) 129 | 130 | (defn fake-client [builder] 131 | (proxy [HttpClient] [] 132 | (newWebSocketBuilder [] builder))) 133 | 134 | (deftest builder 135 | (let [args (atom {}) 136 | fake-client (fake-client (->FakeBuilder args))] 137 | (websocket-builder {:client fake-client 138 | :connect-timeout 3000 139 | :subprotocols ["foo" "bar"] 140 | :headers {"a" "b" 141 | "c" ["d" "e"]}}) 142 | (is (= [[(Duration/ofSeconds 3)]] (-> @args :connectTimeout))) 143 | (is (= [["a" "b"] ["c" "d"] ["c" "e"]] (-> @args :header))) 144 | (is (= "foo" (-> @args :subprotocols ffirst))) 145 | (is (Arrays/equals (into-array String ["bar"]) 146 | (-> @args :subprotocols first second))))) 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # java-http-clj [![CircleCI](https://circleci.com/gh/schmee/java-http-clj.svg?style=svg)](https://circleci.com/gh/schmee/java-http-clj) [![cljdoc badge](https://cljdoc.org/badge/java-http-clj/java-http-clj)](https://cljdoc.org/d/java-http-clj/java-http-clj/CURRENT) 2 | 3 | [clj-http](https://github.com/dakrone/clj-http) is the de-facto standard HTTP client for Clojure. It is an excellent library, but it is also a large dependency since it is based on [Apache HTTP](https://hc.apache.org/httpcomponents-client-ga/). It also doesn't support HTTP/2 (yet). 4 | 5 | Enter java-http-clj. It is inspired by both clj-http and [Ring](https://github.com/ring-clojure/ring/blob/master/SPEC) and built on `java.net.http` that ships with with Java 11. As such it comes with _no_ extra dependencies if you're already using Java 11 and it fully supports HTTP/2 out of the box. 6 | 7 | ## Installation 8 | 9 | [![Current Version](https://clojars.org/java-http-clj/latest-version.svg)](https://clojars.org/java-http-clj) 10 | 11 | java-http-clj requires Clojure 1.9+ and Java 11+. 12 | 13 | ## Documentation 14 | 15 | - [API documentation](https://cljdoc.org/d/java-http-clj/java-http-clj/CURRENT/api/java-http-clj.core) 16 | 17 | - [Specs](https://github.com/schmee/java-http-clj/blob/master/src/java_http_clj/specs.clj) (note: as of version `0.4.3` the specs have moved from `java-http-clj.core` and `java-http-clj.websocket` to `java-http-clj.specs` and must be required separately) 18 | 19 | - [java.net.http Javadoc](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/package-summary.html) 20 | 21 | ## Examples 22 | 23 | First, require the library: 24 | 25 | ```clj 26 | (:require [java-http-clj.core :as http]) 27 | ``` 28 | 29 | The most common HTTP methods (GET, POST, PUT, HEAD, DELETE) have a function of the same name. This function takes three arguments (where the last two are optional): a URL, a request and an options map (refer to [send](https://cljdoc.org/d/java-http-clj/java-http-clj/0.4.3/api/java-http-clj.core#send) docs for details). 30 | 31 | - GET requests 32 | 33 | ```clj 34 | ;; If you don't specify any options defaults are provided 35 | (http/get "http://www.google.com") 36 | 37 | ;; With request options 38 | (http/get "http://www.google.com" {:headers {"Accept" "application/json" 39 | "Accept-Encoding" ["gzip" "deflate"]} 40 | :timeout 2000}) 41 | ``` 42 | 43 | - POST/PUT requests 44 | 45 | ```clj 46 | (http/post "http://www.google.com" {:body "{\"foo\":\"bar\"}"}) 47 | 48 | ;; The request body can be a string, an input stream or a byte array... 49 | (http/post "http://www.google.com" {:body (.getBytes "{\"foo\":\"bar\"}")}) 50 | 51 | ;; ...and you can choose the response body format with the `:as` option 52 | (http/post "http://www.google.com" {:body "{\"foo\":\"bar\"}"} {:as :byte-array}) 53 | ``` 54 | 55 | - Async requests 56 | 57 | To make an async request, use the `send-async` function (currently there is no sugar for async requests): 58 | 59 | ```clj 60 | ;; Returns a java.util.concurrent.CompletableFuture 61 | (http/send-async {:uri "http://www.google.com" :method :get}) 62 | 63 | ;; Takes an optional callback and exception handler 64 | (http/send-async {:uri "http://www.google.com" :method :get} 65 | (fn [r] (do-something-with-response r)) 66 | (fn [e] (println "oops, something blew up"))) 67 | 68 | ``` 69 | 70 | - Options 71 | 72 | All request functions take an `opts` map for customization (refer to [send](https://cljdoc.org/d/java-http-clj/java-http-clj/0.4.3/api/java-http-clj.core#send) docs for details). 73 | 74 | ```clj 75 | ;; Provide a custom client 76 | (def client (http/build-client {:follow-redirects :always})) 77 | (http/send {:uri "http://www.google.com" :method :get} {:client client}) 78 | 79 | ;; Skip map conversion and return the java.net.http.HttpResponse object 80 | user=> (http/send {:uri "http://www.google.com" :method :get} {:raw? true}) 81 | object[jdk.internal.net.http.HttpResponseImpl "0x88edd90" "(GET http://www.google.com) 200"] 82 | ``` 83 | 84 | ## WebSockets 85 | 86 | java-http-clj also includes a WebSocket API. The WebSocket API in java.net.http is based around CompletableFuture and functional interfaces which interop poorly with Clojure. Hence, java-http-clj presents a simplified, synchronous API that covers the basic use-cases. 87 | 88 | The API consists of three methods: `build-websocket`, `send` and `close`. The Java API requires you to maintain a request counter for each invocation, but java-http-clj manages this for you automatically. 89 | 90 | ```clj 91 | ;; Create a websocket 92 | (def ws 93 | (build-websocket 94 | "ws://localhost:8080/ws" 95 | {:on-text (fn [ws string last?] 96 | (println "Received some text!" string)) 97 | :on-binary (fn [ws byte-array last?] 98 | (println "Got some bytes!" (vec byte-array))) 99 | :on-error (fn [ws throwable] 100 | (println "Uh oh!" (.getMessage throwable)))})) 101 | 102 | ;; Send some data (strings, ByteBuffers, byte arrays or something that can be coerced to a string) 103 | and return the websocket 104 | (-> ws 105 | (send "abc") 106 | (send (byte-array [1 2 3])) 107 | (send 123)) 108 | 109 | ;; Close the output of websocket when you are done 110 | (close ws) 111 | ``` 112 | 113 | There is also `build-websocket-async` and `send-async` that return a `CompletableFuture`. These functions are probably best used together with some asynchronous framework that allows you to compose `CompletableFutures` (for example [Manifold](https://github.com/ztellman/manifold)). 114 | 115 | ## Design 116 | 117 | ### Goals 118 | 119 | - Lightweight: zero dependencies and a small codebase 120 | - Performant: minimal overhead compared to direct interop 121 | - Flexible: make everyday use-cases easy and advanced use-cases possible 122 | 123 | ### Non-goals 124 | 125 | - Hide away the Java 126 | - Common use-cases should not require interop, but more advanced uses might require dipping in to the underlying Java API. 127 | - Completeness 128 | - Not everything will be provided through Clojure. For example, most of the classes in `HttpResponse.BodySubscribers` are not available directly through Clojure, since Clojure has its own stream-processing facilities that are more idiomatic than the Java equivalents. 129 | - Input/output conversion 130 | - java-http-clj does not have any built-in facilities for formats such as JSON, Transit or YAML. It provides the choice of "raw data" through the `:as` option but leaves it up to the user to parse the data as they see fit 131 | 132 | If any of this is deal-breaker for you, I recommend clj-http which is a much more fully-featured HTTP client. 133 | 134 | ## License 135 | 136 | Copyright © 2018-2021 John Schmidt 137 | 138 | Released under the MIT License: http://www.opensource.org/licenses/mit-license.php 139 | -------------------------------------------------------------------------------- /src/java_http_clj/specs.clj: -------------------------------------------------------------------------------- 1 | (ns java-http-clj.specs 2 | (:require [clojure.spec.alpha :as s] 3 | [java-http-clj.util :as util]) 4 | (:import [java.net CookieHandler ProxySelector] 5 | [java.net.http 6 | HttpClient 7 | HttpClient$Builder 8 | HttpRequest 9 | HttpRequest$Builder 10 | HttpResponse 11 | WebSocket 12 | WebSocket$Builder 13 | WebSocket$Listener] 14 | [java.nio ByteBuffer] 15 | [java.time Duration] 16 | [java.util.concurrent CompletableFuture Executor] 17 | [javax.net.ssl SSLContext SSLParameters])) 18 | 19 | 20 | ;; ============================== CORE SPECS ============================== 21 | 22 | 23 | (s/def :java-http-clj.core/expect-continue? boolean?) 24 | (s/def :java-http-clj.core/headers (s/map-of string? (s/or :string string? :seq-of-strings (s/+ string?)))) 25 | (s/def :java-http-clj.core/method keyword?) 26 | (s/def :java-http-clj.core/timeout (s/or :millis pos-int? :duration #(instance? Duration %))) 27 | (s/def :java-http-clj.core/uri string?) 28 | (s/def :java-http-clj.core/version #{:http1.1 :http2}) 29 | 30 | (s/def :java-http-clj.core/req-map 31 | (s/keys :req-un [:java-http-clj.core/uri] 32 | :opt-un [:java-http-clj.core/expect-continue? :java-http-clj.core/headers :java-http-clj.core/method :java-http-clj.core/timeout :java-http-clj.core/version])) 33 | 34 | (s/fdef request-builder 35 | :args (s/cat :req-map (s/? :java-http-clj.core/req-map)) 36 | :ret #(instance? HttpRequest$Builder %)) 37 | 38 | (s/fdef build-request 39 | :args (s/cat :req-map (s/? :java-http-clj.core/req-map)) 40 | :ret #(instance? HttpRequest %)) 41 | 42 | (s/def :java-http-clj.core/connect-timeout :java-http-clj.core/timeout) 43 | (s/def :java-http-clj.core/cookie-handler #(instance? CookieHandler %)) 44 | (s/def :java-http-clj.core/executor #(instance? Executor %)) 45 | (s/def :java-http-clj.core/follow-redirects #{:always :default :never}) 46 | (s/def :java-http-clj.core/priority (s/int-in 1 257)) 47 | (s/def :java-http-clj.core/proxy #(instance? ProxySelector %)) 48 | (s/def :java-http-clj.core/ssl-context #(instance? SSLContext %)) 49 | (s/def :java-http-clj.core/ssl-parameters #(instance? SSLParameters %)) 50 | 51 | (s/def :java-http-clj.core/client-opts 52 | (s/keys :opt-un 53 | [:java-http-clj.core/connect-timeout 54 | :java-http-clj.core/cookie-handler 55 | :java-http-clj.core/executor 56 | :java-http-clj.core/follow-redirects 57 | :java-http-clj.core/priority 58 | :java-http-clj.core/proxy 59 | :java-http-clj.core/ssl-context 60 | :java-http-clj.core/ssl-parameters 61 | :java-http-clj.core/version])) 62 | 63 | (s/fdef client-builder 64 | :args (s/cat :opts (s/? :java-http-clj.core/client-opts)) 65 | :ret #(instance? HttpClient$Builder %)) 66 | 67 | (s/fdef build-client 68 | :args (s/cat :opts (s/? :java-http-clj.core/client-opts)) 69 | :ret #(instance? HttpClient %)) 70 | 71 | (s/def :java-http-clj.core/request 72 | (s/or :uri :java-http-clj.core/uri 73 | :req-map :java-http-clj.core/req-map 74 | :raw #(instance? HttpRequest %))) 75 | 76 | (s/def :java-http-clj.core/as #{:byte-array :input-stream :string}) 77 | (s/def :java-http-clj.core/client #(instance? HttpClient %)) 78 | (s/def :java-http-clj.core/raw? boolean?) 79 | 80 | (s/def :java-http-clj.core/send-opts 81 | (s/keys :opt-un [:java-http-clj.core/as :java-http-clj.core/client :java-http-clj.core/raw?])) 82 | 83 | (s/def :java-http-clj.core/body 84 | (s/or :byte-array bytes? 85 | :input-stream #(instance? java.io.InputStream %) 86 | :string string?)) 87 | 88 | (s/def :java-http-clj.core/status 89 | (s/int-in 100 600)) 90 | 91 | (s/def :java-http-clj.core/response-map 92 | (s/keys :req-un [:java-http-clj.core/body :java-http-clj.core/headers :java-http-clj.core/status :java-http-clj.core/version])) 93 | 94 | (s/def :java-http-clj.core/response 95 | (s/or :map :java-http-clj.core/response-map 96 | :raw #(instance? HttpResponse %))) 97 | 98 | (s/fdef send 99 | :args (s/cat :req :java-http-clj.core/request 100 | :opts (s/? :java-http-clj.core/send-opts)) 101 | :ret :java-http-clj.core/response) 102 | 103 | ;; Use `ifn?` instead of `fspec` for the callbacks due to issues like 104 | ;; https://dev.clojure.org/jira/browse/CLJ-1936 and 105 | ;; https://dev.clojure.org/jira/browse/CLJ-2217 106 | (s/fdef send-async 107 | :args (s/alt :req 108 | (s/cat :req :java-http-clj.core/request) 109 | 110 | :req+opts 111 | (s/cat :req :java-http-clj.core/request 112 | :opts :java-http-clj.core/send-opts) 113 | 114 | :req+opts+callbacks 115 | (s/cat :req :java-http-clj.core/request 116 | :opts :java-http-clj.core/send-opts 117 | :callback (s/nilable ifn?) 118 | ; :callback (s/fspec 119 | ; :args (s/cat :response :java-http-clj.core/response) 120 | ; :ret any?) 121 | :ex-handler (s/nilable ifn?))) 122 | ; :ex-handler (s/fspec 123 | ; :args (s/cat :exception #(instance? Throwable %)) 124 | ; :ret any?))) 125 | :ret #(instance? CompletableFuture %)) 126 | 127 | (s/def :java-http-clj.core/req-map-all-optional 128 | (s/keys :opt-un [:java-http-clj.core/uri :java-http-clj.core/expect-continue? :java-http-clj.core/headers :java-http-clj.core/method :java-http-clj.core/timeout :java-http-clj.core/version])) 129 | 130 | (defn spec-shorthand [method] 131 | (let [method-sym (symbol (name 'java-http-clj.core) (name method))] 132 | `(s/fdef ~method-sym 133 | :args ~'(s/alt :uri (s/cat :uri :java-http-clj.core/uri) 134 | :uri+req-map (s/cat :uri :java-http-clj.core/uri 135 | :req-map :java-http-clj.core/req-map-all-optional) 136 | :uri+req-map+opts (s/cat :uri :java-http-clj.core/uri 137 | :req-map :java-http-clj.core/req-map-all-optional 138 | :opts :java-http-clj.core/send-opts)) 139 | :ret :java-http-clj.core/response))) 140 | 141 | (defmacro ^:private spec-all-shorthands [] 142 | `(do ~@(map spec-shorthand util/shorthands))) 143 | 144 | (spec-all-shorthands) 145 | 146 | 147 | ;; ============================== WEBSOCKET SPECS ============================== 148 | 149 | 150 | (s/def :java-http-clj.websocket/websocket 151 | #(instance? WebSocket %)) 152 | 153 | (s/def :java-http-clj.websocket/payload 154 | (s/or :string string? 155 | :byte-array bytes? 156 | :byte-buffer #(instance? ByteBuffer %) 157 | :other any?)) 158 | 159 | (s/fdef send 160 | :args (s/cat 161 | :websocket :java-http-clj.websocket/websocket 162 | :payload :java-http-clj.websocket/payload 163 | :last? (s/? boolean?)) 164 | :ret :java-http-clj.websocket/websocket) 165 | 166 | (s/def :java-http-clj.websocket/on-binary ifn?) 167 | (s/def :java-http-clj.websocket/on-close ifn?) 168 | (s/def :java-http-clj.websocket/on-error ifn?) 169 | (s/def :java-http-clj.websocket/on-open ifn?) 170 | (s/def :java-http-clj.websocket/on-ping ifn?) 171 | (s/def :java-http-clj.websocket/on-pong ifn?) 172 | (s/def :java-http-clj.websocket/on-text ifn?) 173 | 174 | (s/def :java-http-clj.websocket/listener-fns 175 | (s/keys :opt-un 176 | [:java-http-clj.websocket/on-binary 177 | :java-http-clj.websocket/on-close 178 | :java-http-clj.websocket/on-error 179 | :java-http-clj.websocket/on-open 180 | :java-http-clj.websocket/on-ping 181 | :java-http-clj.websocket/on-pong 182 | :java-http-clj.websocket/on-text])) 183 | 184 | (s/fdef websocket-listener 185 | :args (s/cat :listener-fns :java-http-clj.websocket/listener-fns) 186 | :ret #(instance? WebSocket$Listener %)) 187 | 188 | (s/def :java-http-clj.websocket/subprotocols 189 | (s/+ string?)) 190 | 191 | (s/def :java-http-clj.websocket/builder-opts 192 | (s/keys 193 | :opt-un [:java-http-clj.core/client 194 | :java-http-clj.core/connect-timeout 195 | :java-http-clj.core/headers 196 | :java-http-clj.websocket/subprotocols])) 197 | 198 | (s/fdef websocket-builder 199 | :args (s/cat :opts (s/? :java-http-clj.websocket/builder-opts)) 200 | :ret #(instance? WebSocket$Builder %)) 201 | 202 | (s/fdef build-websocket 203 | :args (s/cat :uri :java-http-clj.core/uri 204 | :listener-fns :java-http-clj.websocket/listener-fns 205 | :builder-opts (s/? :java-http-clj.websocket/builder-opts)) 206 | :ret :java-http-clj.websocket/websocket) 207 | 208 | (s/def :java-http-clj.websocket/status-code 209 | (s/int-in 1000 5000)) 210 | 211 | (s/def :java-http-clj.websocket/reason string?) 212 | 213 | (s/def :java-http-clj.websocket/completable-future 214 | #(instance? CompletableFuture %)) 215 | 216 | (s/fdef close 217 | :args (s/alt :default (s/cat :websocket :java-http-clj.websocket/websocket) 218 | :status-code (s/cat :websocket :java-http-clj.websocket/websocket 219 | :status-code :java-http-clj.websocket/status-code) 220 | :status-code+reason (s/cat :websocket :java-http-clj.websocket/websocket 221 | :status-code :java-http-clj.websocket/status-code 222 | :reason :java-http-clj.websocket/reason)) 223 | :ret :java-http-clj.websocket/completable-future) 224 | -------------------------------------------------------------------------------- /src/java_http_clj/websocket.clj: -------------------------------------------------------------------------------- 1 | (ns java-http-clj.websocket 2 | (:refer-clojure :exclude [send]) 3 | (:require [java-http-clj.core :as core] 4 | [java-http-clj.util :as util :refer [add-docstring]]) 5 | (:import [java.net URI] 6 | [java.net.http 7 | HttpClient 8 | WebSocket 9 | WebSocket$Builder 10 | WebSocket$Listener] 11 | [java.nio ByteBuffer] 12 | [java.util.concurrent CompletableFuture])) 13 | 14 | (set! *warn-on-reflection* true) 15 | 16 | (defn websocket-builder 17 | (^WebSocket$Builder [] (websocket-builder {})) 18 | (^WebSocket$Builder [{:keys [client connect-timeout headers subprotocols]}] 19 | (let [^HttpClient client (or client @core/default-client) 20 | builder (.newWebSocketBuilder client)] 21 | (if headers 22 | (doseq [[k v] headers] 23 | (if (sequential? v) 24 | (run! #(.header builder k %) v) 25 | (.header builder k v)))) 26 | (cond-> builder 27 | connect-timeout (.connectTimeout (util/convert-timeout connect-timeout)) 28 | subprotocols (.subprotocols (first subprotocols) (into-array String (rest subprotocols))))))) 29 | 30 | (defn- byte-buffer->byte-array [^ByteBuffer byte-buffer] 31 | (let [ba (byte-array (.capacity byte-buffer))] 32 | (.get byte-buffer ba) 33 | ba)) 34 | 35 | (defn websocket-listener 36 | (^WebSocket$Listener [{:keys [on-binary on-close on-error on-open on-ping on-pong on-text]}] 37 | (let [that (reify WebSocket$Listener)] 38 | (reify WebSocket$Listener 39 | (onBinary [this ws byte-buffer last?] 40 | (if on-binary 41 | (on-binary ws (byte-buffer->byte-array byte-buffer) last?) 42 | (.onBinary that ws byte-buffer last?))) 43 | (onClose [this ws status-code reason] 44 | (if on-close 45 | (on-close ws status-code reason) 46 | (.onClose that ws status-code reason))) 47 | (onError [this ws throwable] 48 | (if on-error 49 | (on-error ws throwable) 50 | (.onError that ws throwable))) 51 | (onOpen [this ws] 52 | (if on-open 53 | (on-open ws) 54 | (.onOpen that ws))) 55 | (onPing [this ws byte-buffer] 56 | (if on-ping 57 | (on-ping ws (byte-buffer->byte-array byte-buffer)) 58 | (.onPing that ws byte-buffer))) 59 | (onPong [this ws byte-buffer] 60 | (if on-pong 61 | (on-pong ws (byte-buffer->byte-array byte-buffer)) 62 | (.onPong that ws byte-buffer))) 63 | (onText [this ws char-seq last?] 64 | (if on-text 65 | (on-text ws (.toString char-seq) last?) 66 | (.onText that ws char-seq last?))))))) 67 | 68 | (defn- wrap-listener-fns [listener-fns] 69 | (let [non-receive-methods #{:on-close :on-error} 70 | inc-and-nil (fn [f] 71 | (fn [& args] 72 | (let [^WebSocket ws (first args)] 73 | (.request ws 1) 74 | (apply f args) 75 | nil))) 76 | return-nil (fn [f] 77 | (fn [& args] 78 | (apply f args) 79 | nil))] 80 | (into {} 81 | (for [[k f] listener-fns] 82 | (if (contains? non-receive-methods k) 83 | [k (return-nil f)] 84 | [k (inc-and-nil f)]))))) 85 | 86 | (defn build-websocket-async 87 | (^CompletableFuture [uri listener-fns] 88 | (build-websocket-async uri listener-fns {})) 89 | (^CompletableFuture [uri listener-fns builder-opts] 90 | (.buildAsync 91 | (websocket-builder builder-opts) 92 | (URI/create uri) 93 | (websocket-listener (wrap-listener-fns listener-fns))))) 94 | 95 | (defprotocol ^:no-doc Send 96 | (-send [this ws last?])) 97 | 98 | (extend-protocol Send 99 | (Class/forName "[B") 100 | (-send [this ^WebSocket ws last?] 101 | (.sendBinary ws (ByteBuffer/wrap this) last?)) 102 | 103 | ByteBuffer 104 | (-send [this ^WebSocket ws last?] 105 | (.sendBinary ws this last?)) 106 | 107 | String 108 | (-send [this ^WebSocket ws last?] 109 | (.sendText ws this last?)) 110 | 111 | Object 112 | (-send [this ^WebSocket ws last?] 113 | (.sendText ws (str this) last?))) 114 | 115 | (defn send-async 116 | (^CompletableFuture [ws payload] 117 | (send-async ws payload true)) 118 | (^CompletableFuture [^WebSocket ws payload last?] 119 | (-send payload ws last?))) 120 | 121 | (defn- join [^CompletableFuture cf] 122 | (.join cf)) 123 | 124 | (def 125 | ^{:arglists '([uri listener-fns] 126 | [uri listener-fns builder-opts]) 127 | :tag WebSocket} 128 | build-websocket 129 | (comp join build-websocket-async)) 130 | 131 | (def 132 | ^{:arglists '([ws payload] [ws payload last?])} 133 | send 134 | (comp join send-async)) 135 | 136 | (defn ^CompletableFuture close 137 | (^CompletableFuture [^WebSocket ws] (.sendClose ws WebSocket/NORMAL_CLOSURE "")) 138 | (^CompletableFuture [^WebSocket ws status-code] (.sendClose ws status-code "")) 139 | (^CompletableFuture [^WebSocket ws status-code reason] (.sendClose ws status-code reason))) 140 | 141 | 142 | ;; ============================== DOCSTRINGS ============================== 143 | 144 | 145 | (add-docstring #'build-websocket 146 | "Builds a new [WebSocket](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html) with the provided options. 147 | 148 | - `uri` - The URI to connect to 149 | - `listener-fns` - see [[websocket-listener]] 150 | - `builder-opts` - see [[websocket-builder]]") 151 | 152 | (add-docstring #'build-websocket-async 153 | "Same as [[build-websocket]], but returns a `CompletableFuture` instead of `WebSocket`.") 154 | 155 | (add-docstring #'websocket-listener 156 | "Builds a [Websocket.Listener](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html). 157 | 158 | listener-fns is a map of keyword to function: 159 | 160 | - `:on-binary` - See [onBinary](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onBinary%28java.net.http.WebSocket,java.nio.ByteBuffer,boolean%29). The `data` argument is converted from a `ByteBuffer` to a byte array before being passed to the function. 161 | - `:on-close` - See [onClose](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onClose%28java.net.http.WebSocket,int,java.lang.String%29) 162 | - `:on-error` - See [onError](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onError%28java.net.http.WebSocket,java.lang.Throwable%29) 163 | - `:on-open` - See [onOpen](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onOpen%28java.net.http.WebSocket%29) 164 | - `:on-ping` - See [onPing](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onPing%28java.net.http.WebSocket,java.nio.ByteBuffer%29). The `data` argument is converted from a `ByteBuffer` to a byte array before being passed to the function. 165 | - `:on-pong` - See [onPong](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onPong%28java.net.http.WebSocket,java.nio.ByteBuffer%29). The `data` argument is converted from a `ByteBuffer` to a byte array before being passed to the function. 166 | - `:on-text` - See [onText](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Listener.html#onText%28java.net.http.WebSocket,java.lang.CharSequence,boolean%29). The `data` argument is converted from a `CharSequence` to a string before being passed to the function.") 167 | 168 | (add-docstring #'websocket-builder 169 | "Builds a [Websocket.Builder](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.Builder.html). 170 | 171 | `opts` is a map containing one of the following keywords: 172 | 173 | - `:client` - the [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html) to create the connect with, defaults to [[default-client]] 174 | - `:connect-timeout` - connection timeout in milliseconds or a `java.time.Duration` 175 | - `:headers` - the HTTP headers, a map where keys are strings and values are strings or a list of strings 176 | - `:subprotocols` - a string sequence of subprotocols to use in order of preferences") 177 | 178 | (add-docstring #'send 179 | "Synchronously send data over the websocket and then returns the websocket. `payload` can be any of: 180 | 181 | - a string - sent with [sendText](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html#sendText%28java.lang.CharSequence,boolean%29) 182 | - a byte array - converted into a ByteBuffer and sent with [sendBinary](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html#sendBinary%28java.nio.ByteBuffer,boolean%29) 183 | - a ByteBufer - sent with [sendBinary](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html#sendBinary%28java.nio.ByteBuffer,boolean%29) 184 | 185 | Any other argument type will be coerced to a string with `str` and sent with [sendText](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html#sendText%28java.lang.CharSequence,boolean%29). 186 | 187 | `last?` is a boolean that indicates whether this invocation completes the message. Defaults to `true`.") 188 | 189 | (add-docstring #'send-async 190 | "Same as [[send]], but returns a `CompletableFuture` instead of `WebSocket`.") 191 | 192 | (add-docstring #'close 193 | "Closes the output of the websocket with the supplied status code and reason. If not provided, `status-code` defaults to 1000 (normal closure) and `reason` defaults to empty string. 194 | 195 | Equivalent to [sendClose](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/WebSocket.html#sendClose%28int,java.lang.String%29).") 196 | -------------------------------------------------------------------------------- /src/java_http_clj/core.clj: -------------------------------------------------------------------------------- 1 | (ns java-http-clj.core 2 | (:refer-clojure :exclude [send get]) 3 | (:require [clojure.string :as str] 4 | [java-http-clj.util :as util :refer [add-docstring]]) 5 | (:import [java.net URI] 6 | [java.net.http 7 | HttpClient 8 | HttpClient$Builder 9 | HttpClient$Redirect 10 | HttpClient$Version 11 | HttpRequest 12 | HttpRequest$BodyPublishers 13 | HttpRequest$Builder 14 | HttpResponse 15 | HttpResponse$BodyHandlers] 16 | [java.util.concurrent CompletableFuture] 17 | [java.util.function Function Supplier])) 18 | 19 | (set! *warn-on-reflection* true) 20 | 21 | (defn- version-keyword->version-enum [version] 22 | (case version 23 | :http1.1 HttpClient$Version/HTTP_1_1 24 | :http2 HttpClient$Version/HTTP_2)) 25 | 26 | (defn- convert-follow-redirect [redirect] 27 | (case redirect 28 | :always HttpClient$Redirect/ALWAYS 29 | :never HttpClient$Redirect/NEVER 30 | :normal HttpClient$Redirect/NORMAL)) 31 | 32 | (defn client-builder 33 | (^HttpClient$Builder [] 34 | (client-builder {})) 35 | (^HttpClient$Builder [opts] 36 | (let [{:keys [connect-timeout 37 | cookie-handler 38 | executor 39 | follow-redirects 40 | priority 41 | proxy 42 | ssl-context 43 | ssl-parameters 44 | version]} opts] 45 | (cond-> (HttpClient/newBuilder) 46 | connect-timeout (.connectTimeout (util/convert-timeout connect-timeout)) 47 | cookie-handler (.cookieHandler cookie-handler) 48 | executor (.executor executor) 49 | follow-redirects (.followRedirects (convert-follow-redirect follow-redirects)) 50 | priority (.priority priority) 51 | proxy (.proxy proxy) 52 | ssl-context (.sslContext ssl-context) 53 | ssl-parameters (.sslParameters ssl-parameters) 54 | version (.version (version-keyword->version-enum version)))))) 55 | 56 | (defn build-client 57 | (^HttpClient [] (.build (client-builder))) 58 | (^HttpClient [opts] (.build (client-builder opts)))) 59 | 60 | (def ^HttpClient default-client 61 | (delay (HttpClient/newHttpClient))) 62 | 63 | (def ^:private byte-array-class 64 | (Class/forName "[B")) 65 | 66 | (defn- input-stream-supplier [s] 67 | (reify Supplier 68 | (get [this] s))) 69 | 70 | (defn- convert-body-publisher [body] 71 | (cond 72 | (nil? body) 73 | (HttpRequest$BodyPublishers/noBody) 74 | 75 | (string? body) 76 | (HttpRequest$BodyPublishers/ofString body) 77 | 78 | (instance? java.io.InputStream body) 79 | (HttpRequest$BodyPublishers/ofInputStream (input-stream-supplier body)) 80 | 81 | (instance? byte-array-class body) 82 | (HttpRequest$BodyPublishers/ofByteArray body))) 83 | 84 | (def ^:private convert-headers-xf 85 | (mapcat 86 | (fn [[k v :as p]] 87 | (if (sequential? v) 88 | (interleave (repeat k) v) 89 | p)))) 90 | 91 | (defn- method-keyword->str [method] 92 | (str/upper-case (name method))) 93 | 94 | (defn request-builder ^HttpRequest$Builder [opts] 95 | (let [{:keys [expect-continue? 96 | headers 97 | method 98 | timeout 99 | uri 100 | version 101 | body]} opts] 102 | (cond-> (HttpRequest/newBuilder) 103 | (some? expect-continue?) (.expectContinue expect-continue?) 104 | (seq headers) (.headers (into-array String (eduction convert-headers-xf headers))) 105 | method (.method (method-keyword->str method) (convert-body-publisher body)) 106 | timeout (.timeout (util/convert-timeout timeout)) 107 | uri (.uri (URI/create uri)) 108 | version (.version (version-keyword->version-enum version))))) 109 | 110 | (defn build-request 111 | (^HttpRequest [] (.build (request-builder {}))) 112 | (^HttpRequest [req-map] (.build (request-builder req-map)))) 113 | 114 | (def ^:private bh-of-string (HttpResponse$BodyHandlers/ofString)) 115 | (def ^:private bh-of-input-stream (HttpResponse$BodyHandlers/ofInputStream)) 116 | (def ^:private bh-of-byte-array (HttpResponse$BodyHandlers/ofByteArray)) 117 | 118 | (defn- convert-body-handler [mode] 119 | (case mode 120 | nil bh-of-string 121 | :string bh-of-string 122 | :input-stream bh-of-input-stream 123 | :byte-array bh-of-byte-array)) 124 | 125 | (defn- version-enum->version-keyword [^HttpClient$Version version] 126 | (case (.name version) 127 | "HTTP_1_1" :http1.1 128 | "HTTP_2" :http2)) 129 | 130 | (defn response->map [^HttpResponse resp] 131 | {:status (.statusCode resp) 132 | :body (.body resp) 133 | :version (-> resp .version version-enum->version-keyword) 134 | :headers (into {} 135 | (map (fn [[k v]] [k (if (> (count v) 1) (vec v) (first v))])) 136 | (.map (.headers resp)))}) 137 | 138 | 139 | (def ^:private ^Function resp->ring-function 140 | (util/clj-fn->function response->map)) 141 | 142 | (defn- convert-request [req] 143 | (cond 144 | (map? req) (build-request req) 145 | (string? req) (build-request {:uri req}) 146 | (instance? HttpRequest req) req)) 147 | 148 | (defn send 149 | ([req] 150 | (send req {})) 151 | ([req {:keys [as client raw?] :as opts}] 152 | (let [^HttpClient client (or client @default-client) 153 | req' (convert-request req) 154 | resp (.send client req' (convert-body-handler as))] 155 | (if raw? resp (response->map resp))))) 156 | 157 | (defn send-async 158 | (^CompletableFuture [req] 159 | (send-async req {} nil nil)) 160 | (^CompletableFuture [req opts] 161 | (send-async req opts nil nil)) 162 | (^CompletableFuture [req {:keys [as client raw?] :as opts} callback ex-handler] 163 | (let [^HttpClient client (or client @default-client) 164 | req' (convert-request req)] 165 | (cond-> (.sendAsync client req' (convert-body-handler as)) 166 | (not raw?) (.thenApply resp->ring-function) 167 | callback (.thenApply (util/clj-fn->function callback)) 168 | ex-handler (.exceptionally (util/clj-fn->function ex-handler)))))) 169 | 170 | (defn- shorthand-docstring [method] 171 | (str "Sends a " (method-keyword->str method) " request to `uri`. 172 | 173 | See [[send]] for a description of `req-map` and `opts`.")) 174 | 175 | (defn- defshorthand [method] 176 | `(defn ~(symbol (name method)) 177 | ~(shorthand-docstring method) 178 | (~['uri] 179 | (send ~{:uri 'uri :method method} {})) 180 | (~['uri 'req-map] 181 | (send (merge ~'req-map ~{:uri 'uri :method method}) {})) 182 | (~['uri 'req-map 'opts] 183 | (send (merge ~'req-map ~{:uri 'uri :method method}) ~'opts)))) 184 | 185 | (defmacro ^:private def-all-shorthands [] 186 | `(do ~@(map defshorthand util/shorthands))) 187 | 188 | (def-all-shorthands) 189 | 190 | 191 | ;; ============================== DOCSTRINGS ============================== 192 | 193 | 194 | (add-docstring #'default-client 195 | "Used for requests unless a client is explicitly passed. Equal to [HttpClient.newHttpClient()](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html#newHttpClient%28%29).") 196 | 197 | (add-docstring #'client-builder 198 | "Same as [[build-client]], but returns a [HttpClient.Builder](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html) instead of a [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html). 199 | 200 | See [[build-client]] for a description of `opts`.") 201 | 202 | 203 | (add-docstring #'build-client 204 | "Builds a client with the supplied options. See [HttpClient.Builder](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html) for a more detailed description of the options. 205 | 206 | The `opts` map takes the following keys: 207 | 208 | - `:connect-timeout` - connection timeout in milliseconds or a `java.time.Duration` 209 | - `:cookie-handler` - a [java.net.CookieHandler](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/CookieHandler.html) 210 | - `:executor` - a [java.util.concurrent.Executor](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Executor.html) 211 | - `:follow-redirects` - one of `:always`, `:never` and `:normal`. Maps to the corresponding [HttpClient.Redirect](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Redirect.html) enum. 212 | - `:priority` - the [priority](https://developers.google.com/web/fundamentals/performance/http2/#stream_prioritization) of the request (only used for HTTP/2 requests) 213 | - `:proxy` - a [java.net.ProxySelector](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/ProxySelector.html) 214 | - `:ssl-context` - a [javax.net.ssl.SSLContext](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLContext.html) 215 | - `:ssl-parameters` - a [javax.net.ssl.SSLParameters](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLParameters.html) 216 | - `:version` - the HTTP protocol version, one of `:http1.1` or `:http2` 217 | 218 | Equivalent to `(.build (client-builder opts))`.") 219 | 220 | (add-docstring #'request-builder 221 | "Same as [[build-request]], but returns a [HttpRequest.Builder](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html) instead of a [HttpRequest](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.html).") 222 | 223 | (add-docstring #'build-request 224 | "Builds a [java.net.http.HttpRequest](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.html) from a map. 225 | 226 | See [[send]] for a description of `req-map`. 227 | 228 | Equivalent to `(.build (request-builder req-map))`.") 229 | 230 | (add-docstring #'send 231 | "Sends a HTTP request and blocks until a response is returned or the request 232 | takes longer than the specified `timeout`. If the request times out, a [HttpTimeoutException](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpTimeoutException.html) 233 | is thrown. 234 | 235 | The `req` parameter can be a either string URL, a request map, or a [java.net.http.HttpRequest](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.html). 236 | 237 | The request map takes the following keys: 238 | 239 | - `:body` - the request body. Can be a string, a primitive Java byte array or a java.io.InputStream. 240 | - `:expect-continue?` - See the [javadoc](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html#expectContinue%28boolean%29) 241 | - `:headers` - the HTTP headers, a map where keys are strings and values are strings or a list of strings 242 | - `:method` - the HTTP method as a keyword (e.g `:get`, `:put`, `:post`) 243 | - `:timeout` - the request timeout in milliseconds or a `java.time.Duration` 244 | - `:uri` - the request uri 245 | - `:version` - the HTTP protocol version, one of `:http1.1` or `:http2` 246 | 247 | `opts` is a map containing one of the following keywords: 248 | 249 | - `:as` - converts the response body to one of the following formats: 250 | - `:string` - a java.lang.String (default) 251 | - `:byte-array` - a Java primitive byte array. 252 | - `:input-stream` - a java.io.InputStream. 253 | 254 | - `:client` - the [HttpClient](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html) to use for the request. If not provided the [[default-client]] will be used. 255 | 256 | - `:raw?` - if true, skip the Ring format conversion and return the [HttpResponse](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpResponse.html)") 257 | 258 | (add-docstring #'send-async 259 | "Sends a request asynchronously and immediately returns a [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html). Converts the 260 | eventual response to a map as per [[response->map]]. 261 | 262 | See [[send]] for a description of `req` and `opts`. 263 | 264 | `callback` is a one argument function that will be applied to the response on completion. 265 | 266 | `ex-handler` is a one argument function that will be called if an exception is thrown anywhere during the request.") 267 | 268 | (add-docstring #'response->map 269 | "Converts a [HttpResponse](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpResponse.html) into a map. 270 | 271 | The response map contains the following keys: 272 | 273 | - `:body` - the response body 274 | - `:headers` - the HTTP headers, a map where keys are strings and values are strings or a list of strings 275 | - `:status` - the HTTP status code 276 | - `:version` - the HTTP protocol version, one of `:http1.1` or `:http2`") 277 | --------------------------------------------------------------------------------