{"id":13609165,"url":"https://github.com/babashka/http-client","last_synced_at":"2025-05-15T18:03:14.981Z","repository":{"id":65589983,"uuid":"583441969","full_name":"babashka/http-client","owner":"babashka","description":"HTTP client for Clojure and Babashka built on java.net.http","archived":false,"fork":false,"pushed_at":"2025-03-17T19:09:35.000Z","size":172,"stargazers_count":130,"open_issues_count":1,"forks_count":14,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-31T22:18:09.887Z","etag":null,"topics":["babashka","clojure"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/babashka.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-29T19:45:27.000Z","updated_at":"2025-03-24T00:21:42.000Z","dependencies_parsed_at":"2024-02-13T12:39:17.010Z","dependency_job_id":"a0b93563-65e1-4651-8d24-18af4ea66e26","html_url":"https://github.com/babashka/http-client","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babashka%2Fhttp-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babashka%2Fhttp-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babashka%2Fhttp-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babashka%2Fhttp-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/babashka","download_url":"https://codeload.github.com/babashka/http-client/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247744333,"owners_count":20988783,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["babashka","clojure"],"created_at":"2024-08-01T19:01:32.949Z","updated_at":"2025-05-15T18:03:14.973Z","avatar_url":"https://github.com/babashka.png","language":"Clojure","readme":"# http-client\n\n[![Clojars Project](https://img.shields.io/clojars/v/org.babashka/http-client.svg)](https://clojars.org/org.babashka/http-client)\n[![bb built-in](https://raw.githubusercontent.com/babashka/babashka/master/logo/built-in-badge.svg)](https://book.babashka.org#badges)\n\nAn HTTP client for Clojure and Babashka built on `java.net.http`.\n\n## API\n\nSee [API.md](API.md).\n\n\u003e NOTE: The `babashka.http-client` library is built-in as of babashka version 1.1.171.\n\n\u003e TIP: We test and support `babashka.http-client` on Clojure v1.10 and above.\n\n## Installation\n\nUse as a dependency in `deps.edn` or `bb.edn`:\n\n``` clojure\norg.babashka/http-client {:mvn/version \"0.4.22\"}\n```\n\n## Rationale\n\nBabashka has several built-in options for making HTTP requests, including:\n\n- [babashka.curl](https://github.com/babashka/babashka.curl)\n- [http-kit](https://github.com/http-kit/http-kit)\n- [java.net.http](https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/package-summary.html)\n\nIn addition, it allows to use several libraries to be used as a dependency:\n\n- [java-http-clj](https://github.com/schmee/java-http-clj)\n- [hato](https://github.com/gnarroway/hato)\n- [clj-http-lite](https://github.com/clj-commons/clj-http-lite)\n\nThe built-in clients come with their own trade-offs. E.g. babashka.curl shells\nout to `curl` which on Windows requires your local `curl` to be\nupdated. Http-kit buffers the entire response in memory. Using `java.net.http`\ndirectly can be a bit verbose.\n\nBabashka's http-client aims to be a good default for most scripting use cases\nand is built on top of `java.net.http` and can be used as a dependency-free JVM\nlibrary as well. The API is mostly compatible with babashka.curl so it can be\nused as a drop-in replacement. The other built-in solutions will not be removed\nany time soon.\n\n## Usage\n\nThe APIs in this library are mostly compatible with\n[babashka.curl](https://github.com/babashka/babashka.curl), which is in turn\ninspired by libraries like [clj-http](https://github.com/dakrone/clj-http).\n\n``` clojure\n(require '[babashka.http-client :as http])\n(require '[clojure.java.io :as io]) ;; optional\n(require '[cheshire.core :as json]) ;; optional\n```\n\n### GET\n\nSimple `GET` request:\n\n``` clojure\n(http/get \"https://httpstat.us/200\")\n;;=\u003e {:status 200, :body \"200 OK\", :headers { ... }}\n```\n\n### Headers\n\nPassing headers:\n\n``` clojure\n(def resp (http/get \"https://httpstat.us/200\" {:headers {\"Accept\" \"application/json\"}}))\n(json/parse-string (:body resp)) ;;=\u003e {\"code\" 200, \"description\" \"OK\"}\n```\n\nHeaders may be provided as keywords as well:\n\n``` clojure\n{:headers {:content-type \"application/json\"}}\n```\n\n### Query parameters\n\nQuery parameters:\n\n``` clojure\n(-\u003e\n  (http/get \"https://postman-echo.com/get\" {:query-params {\"q\" \"clojure\"}})\n  :body\n  (json/parse-string true)\n  :args)\n;;=\u003e {:q \"clojure\"}\n```\n\nTo send multiple params to the same key:\n```clojure\n;; https://postman-echo.com/get?q=clojure\u0026q=curl\n\n(-\u003e (http/get \"https://postman-echo.com/get\" {:query-params {:q [\"clojure\" \"curl\"]}})\n    :body (json/parse-string true) :args)\n;;=\u003e {:q [\"clojure\" \"curl\"]}\n```\n\n### POST\n\nA `POST` request with a `:body`:\n\n``` clojure\n(def resp (http/post \"https://postman-echo.com/post\" {:body \"From Clojure\"}))\n(json/parse-string (:body resp)) ;;=\u003e {\"args\" {}, \"data\" \"From Clojure\", ...}\n```\n\nA `POST` request with a JSON `:body`:\n\n``` clojure\n(def resp (http/post \"https://postman-echo.com/post\"\n                     {:headers {:content-type \"application/json\"}\n                      :body (json/encode {:a 1 :b \"2\"})}))\n(:data (json/parse-string (:body resp) true)) ;;=\u003e {:a 1, :b \"2\"}\n```\n\nPosting a file as a `POST` body:\n\n``` clojure\n(:status (http/post \"https://postman-echo.com/post\" {:body (io/file \"README.md\")}))\n;; =\u003e 200\n```\n\nPosting a stream as a `POST` body:\n\n``` clojure\n(:status (http/post \"https://postman-echo.com/post\" {:body (io/input-stream \"README.md\")}))\n;; =\u003e 200\n```\n\nPosting form params:\n\n``` clojure\n(:status (http/post \"https://postman-echo.com/post\" {:form-params {\"name\" \"Michiel\"}}))\n;; =\u003e 200\n```\n\n### Basic auth\n\nBasic auth:\n\n``` clojure\n(:body (http/get \"https://postman-echo.com/basic-auth\" {:basic-auth [\"postman\" \"password\"]}))\n;; =\u003e \"{\\\"authenticated\\\":true}\"\n```\n### Oauth token\n\nOauth token:\n\n``` clojure\n(:body (http/get \"https://httpbin.org/bearer\" {:oauth-token \"qwertyuiop\"}))\n;; =\u003e \"{\\n  \\\"authenticated\\\": true, \\n  \\\"token\\\": \\\"qwertyuiop\\\"\\n}\\n\"\n```\n\n### Streaming\n\nWith `:as :stream`:\n\n``` clojure\n(:body (http/get \"https://github.com/babashka/babashka/raw/master/logo/icon.png\"\n    {:as :stream}))\n```\n\nwill return the raw input stream.\n\n### Download binary\n\nDownload a binary file:\n\n``` clojure\n(io/copy\n  (:body (http/get \"https://github.com/babashka/babashka/raw/master/logo/icon.png\"\n    {:as :stream}))\n  (io/file \"icon.png\"))\n(.length (io/file \"icon.png\"))\n;;=\u003e 7748\n```\n\nTo obtain an in-memory byte array you can use `:as :bytes`.\n\n### URI construction\n\nUsing the verbose `:uri` API for fine grained (and safer) URI construction:\n\n``` clojure\n(-\u003e (http/request {:uri {:scheme \"https\"\n                           :host   \"httpbin.org\"\n                           :port   443\n                           :path   \"/get\"\n                           :query  \"q=test\"}})\n    :body\n    (json/parse-string true))\n;;=\u003e\n{:args {:q \"test\"},\n :headers\n {:Accept \"*/*\",\n  :Host \"httpbin.org\",\n  :User-Agent \"Java-http-client/11.0.17\"\n  :X-Amzn-Trace-Id\n  \"Root=1-5e63989e-7bd5b1dba75e951a84d61b6a\"},\n :origin \"46.114.35.45\",\n :url \"https://httpbin.org/get?q=test\"}\n```\n\n### Custom client\n\nThe default client in babashka.http-client is constructed conceptually as follows:\n\n``` clojure\n(def client (http/client http/default-client-opts))\n```\n\nTo pass more options in addition to the default options, you can use `http/default-client-opts` and associate more options:\n\n``` clojure\n(def client (http/client (assoc-in http/default-client-opts [:ssl-context :insecure] true)))\n```\n\nThen use the custom client with HTTP requests:\n\n``` clojure\n(http/get \"https://clojure.org\" {:client client})\n```\n\n### Redirects\n\nThe default client is configured to always follow redirects. To opt out of this behaviour, construct a custom client:\n\n```clojure\n(:status (http/get \"https://httpstat.us/302\" {:client (http/client {:follow-redirects :never})}))\n;; =\u003e 302\n(:status (http/get \"https://httpstat.us/302\" {:client (http/client {:follow-redirects :always})}))\n;; =\u003e 200\n```\n\n### Exceptions\n\nAn `ExceptionInfo` will be thrown for all HTTP response status codes other than `#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}`.\n\n```clojure\nuser=\u003e (http/get \"https://httpstat.us/404\")\nExecution error (ExceptionInfo) at babashka.http-client.interceptors/fn (interceptors.clj:194).\nExceptional status code: 404\n ```\n\nTo opt out of an exception being thrown, set `:throw` to false.\n\n```clojure\n(:status (http/get \"https://httpstat.us/404\" {:throw false}))\n;;=\u003e 404\n```\n\n### Multipart\n\nTo perform a multipart request, supply `:multipart` with a sequence of maps with the following options:\n\n- `:name`: The name of the param\n- `:part-name`: Override for `:name`\n- `:content`: The part's data. May be string or something that can be fed into `clojure.java.io/input-stream`\n- `:file-name`: The part's file name. If the `:content` is a file, the name of the file will be used, unless `:file-name` is set.\n- `:content-type`: The part's content type. By default, if `:content` is a string it will be `text/plain; charset=UTF-8`; if `:content` is a file it will attempt to guess the best content type or fallback to `application/octet-stream`.\n\nAn example request:\n\n``` clojure\n(http/post \"https://postman-echo.com/post\"\n           {:multipart [{:name \"title\" :content \"My Title\"}\n                        {:name \"Content/type\" :content \"image/jpeg\"}\n                        {:name \"file\" :content (io/file \"foo.jpg\") :file-name \"foobar.jpg\"}]})\n```\n\n### Compression\n\nTo accept gzipped or zipped responses, use:\n\n``` clojure\n(http/get \"https://api.stackexchange.com/2.2/sites\"\n  {:headers {\"Accept-Encoding\" [\"gzip\" \"deflate\"]}})\n```\n\nThe above server only serves compressed responses, so if you remove the header, the request will fail.\nAccepting compressed responses may become the default in a later version of this library.\n\n### Interceptors\n\nBabashka http-client interceptors are similar to Pedestal interceptors. They are maps of `:name` (a string), `:request` (a function), `:response` (a function).\nAn example is shown in this test:\n\n``` clojure\n(deftest interceptor-test\n  (let [json-interceptor\n        {:name ::json\n         :description\n         \"A request with `:as :json` will automatically get the\n         \\\"application/json\\\" accept header and the response is decoded as JSON.\"\n         :request (fn [request]\n                    (if (= :json (:as request))\n                      (-\u003e (assoc-in request [:headers :accept] \"application/json\")\n                          ;; Read body as :string\n                          ;; Mark request as amenable to json decoding\n                          (assoc :as :string ::json true))\n                      request))\n         :response (fn [response]\n                     (if (get-in response [:request ::json])\n                       (update response :body #(json/parse-string % true))\n                       response))}\n        ;; Add json interceptor add beginning of chain\n        ;; It will be the first to see the request and the last to see the response\n        interceptors (cons json-interceptor interceptors/default-interceptors)\n        ]\n    (testing \"interceptors on request\"\n      (let [resp (http/get \"https://httpstat.us/200\"\n                             {:interceptors interceptors\n                              :as :json})]\n        (is (= 200 (-\u003e resp :body\n                       ;; response as JSON\n                       :code)))))))\n```\n\nA `:request` function is executed when the request is built and the `:response`\nfunction is executed on the response. Default interceptors are in\n`babashka.http-client.interceptors/default-interceptors`.  Interceptors can be\nconfigured on the level of requests by passing a modified `:interceptors`\nchain.\n\n#### Changing an existing interceptor\n\nIn this example we change the `throw-on-exceptional-status-code` interceptor to not throw on a `404` status code:\n\n``` clojure\n(require '[babashka.http-client :as http]\n         '[babashka.http-client.interceptors :as i])\n\n(def unexceptional-statuses\n  (conj #{200 201 202 203 204 205 206 207 300 301 302 303 304 307}\n        ;; we also don't throw on 404\n        404))\n\n(def my-throw-on-exceptional-status-code\n  \"Response: throw on exceptional status codes\"\n  {:name ::throw-on-exceptional-status-code\n   :response (fn [resp]\n               (if-let [status (:status resp)]\n                 (if (or (false? (some-\u003e resp :request :throw))\n                         (contains? unexceptional-statuses status))\n                   resp\n                   (throw (ex-info (str \"Exceptional status code: \" status) resp)))\n                 resp))})\n\n(def my-interceptors\n  (mapv (fn [i]\n         (if (= ::i/throw-on-exceptional-status-code\n                (:name i))\n           my-throw-on-exceptional-status-code\n           i))\n        i/default-interceptors))\n\n(def my-response\n  (http/get \"https://postman-echo.com/get/404\" {:interceptors my-interceptors}))\n\n(prn (:status my-response)) ;; 404\n```\n\n#### Testing interceptors\n\nFor testing interceptors it can be useful to use the `:client` option in combination with a\nClojure function. When passing a function, the request won't be converted to a\n`java.net.http.Request` but just passed as a ring request to the function. The\nfunction is expected to return a ring response:\n\n``` clojure\n(http/get \"https://clojure.org\" {:client (fn [req] {:body 200})})\n```\n\n### Async\n\nTo execute request asynchronously, use `:async true`. The response will be a\n`CompletableFuture` with the response map.\n\n``` clojure\n(-\u003e (http/get \"https://clojure.org\" {:async true}) deref :status)\n;;=\u003e 200\n```\n\n### Timeouts\n\nTwo different timeouts can be set:\n\n- The connection timeout, `:connect-timeout`, in `http/client`\n- The request `:timeout` in `http/request`\n\nAlternatively you can use `:async` + `deref` with a timeout + default value:\n\n```\n(let [resp (http/get \"https://httpstat.us/200?sleep=5000\" {:async true})] (deref resp 1000 ::too-late))\n;;=\u003e :user/too-late\n```\n\n## Logging and debug\n\nIf you need to debug HTTP requests you need to add a JVM system property with some debug options:\n`\"-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all..],content,ssl,trace,channel\"` \n\nOne way to handle that with tools-deps is to add an alias with `:jvm-opts` option.\n\nHere is a code snippet for `deps.edn`\n```clojure\n{\n;; REDACTED\n:aliases {\n :debug\n {:jvm-opts\n  [;; enable logging for java.net.http\n  \"-Djdk.httpclient.HttpClient.log=errors,requests,headers,frames[:control:data:window:all..],content,ssl,trace,channel\"]}\n}}\n```\n\n## Test\n\n``` clojure\n$ bb test:clj\n$ bb test:bb\n```\n\n## Credits\n\nThis library has borrowed liberally from [java-http-clj](https://github.com/schmee/java-http-clj) and [hato](https://github.com/gnarroway/hato), both available under the MIT license.\n\n## License\n\nCopyright © 2022 - 2023 Michiel Borkent\n\nDistributed under the MIT License. See LICENSE.\n","funding_links":[],"categories":["Clojure"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbabashka%2Fhttp-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbabashka%2Fhttp-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbabashka%2Fhttp-client/lists"}