{"id":13801426,"url":"https://github.com/gnarroway/hato","last_synced_at":"2025-10-22T02:47:26.825Z","repository":{"id":37412954,"uuid":"191013415","full_name":"gnarroway/hato","owner":"gnarroway","description":"An HTTP client for Clojure, wrapping JDK 11's HttpClient","archived":false,"fork":false,"pushed_at":"2024-07-19T14:39:57.000Z","size":159,"stargazers_count":390,"open_issues_count":18,"forks_count":29,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-23T09:42:39.928Z","etag":null,"topics":[],"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/gnarroway.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":"2019-06-09T14:01:25.000Z","updated_at":"2025-04-08T21:46:04.000Z","dependencies_parsed_at":"2024-01-05T21:51:25.956Z","dependency_job_id":"8e68d4ea-8033-4d5b-a8da-0fc382ae9222","html_url":"https://github.com/gnarroway/hato","commit_stats":{"total_commits":106,"total_committers":12,"mean_commits":8.833333333333334,"dds":0.160377358490566,"last_synced_commit":"7862f9e3ab692ea499b935842978688831e61bfd"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnarroway%2Fhato","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnarroway%2Fhato/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnarroway%2Fhato/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnarroway%2Fhato/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gnarroway","download_url":"https://codeload.github.com/gnarroway/hato/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253932820,"owners_count":21986454,"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":[],"created_at":"2024-08-04T00:01:22.693Z","updated_at":"2025-10-22T02:47:26.806Z","avatar_url":"https://github.com/gnarroway.png","language":"Clojure","funding_links":[],"categories":["HTTP","网络编程"],"sub_categories":["Spring Cloud框架"],"readme":"# hato\n\n[![Clojars Project](https://img.shields.io/clojars/v/hato.svg)](https://clojars.org/hato)\n\nAn HTTP client for Clojure, wrapping JDK 11's [HttpClient](https://openjdk.java.net/groups/net/httpclient/intro.html).\n\nIt supports both HTTP/1.1 and HTTP/2, with synchronous and asynchronous execution modes as well as websockets.\n\nIn general, it will feel familiar to users of http clients like [clj-http](https://github.com/dakrone/clj-http).\nThe API is designed to be idiomatic and to make common tasks convenient, whilst\nstill allowing the underlying HttpClient to be configured via native Java objects.\n\n## Status\n\nhato has a stable API and is used in production for both synchronous and asynchronous use cases.\nPlease try it out and raise any issues you may find.\n\n## Installation\n\nhato requires JDK 11 and above. If you are running an older version of Java, please look at [clj-http](https://github.com/dakrone/clj-http).\n\nFor Leiningen, add this to your project.clj\n\n```clojure\n[hato \"1.0.0\"]\n```\n\n## Quickstart\n\nThe main client is available in `hato.client`.\n\nRequire it to get started and make a request:\n\n```clojure\n\n(ns my.app\n  (:require [hato.client :as hc]))\n\n  (hc/get \"https://httpbin.org/get\")\n  ; =\u003e\n  ; {:request-time 112\n  ;  :status 200\n  ;  :body \"{\\\"url\\\" ...}\"\n  ;  ...}\n```\n\n## Usage\n\n### Building a client\n\nGenerally, you want to make a reusable client first. This will give you nice things like\npersistent connections and connection pooling.\n\nThis can be done with `build-http-client`:\n\n```clojure\n; Build the client\n(def c (hc/build-http-client {:connect-timeout 10000\n                              :redirect-policy :always}))\n\n; Use it for multiple requests\n(hc/get \"https://httpbin.org/get\" {:http-client c})\n(hc/head \"https://httpbin.org/head\" {:http-client c})\n```\n\n#### build-http-client options\n\n`authenticator` Used for non-preemptive basic authentication. See the `basic-auth` request\n  option for pre-emptive authentication. Accepts:\n\n - A map of `{:user \"username\" :pass \"password\"}`\n - a [`java.net.Authenticator`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/Authenticator.html)\n\n`cookie-handler` a [`java.net.CookieHandler`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/CookieHandler.html)\n  if you need full control of your cookies. See `cookie-policy` for a more convenient option.\n\n`cookie-policy` Determines whether to accept cookies. The `cookie-handler` option will take precedence if it is set.\n  If an invalid option is provided, a CookieManager with the default policy (original-server)\n  will be created. Valid options:\n\n - `:none` Accepts no cookies\n - `:all`  Accepts all cookies\n - `:original-server` (default) Accepts cookies from original server\n - An implementation of [`java.net.CookiePolicy`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/CookiePolicy.html).\n\n`connect-timeout` Timeout to making a connection, in milliseconds (default: unlimited).\n\n`executor` Sets the thread executor.\n\n`redirect-policy` Sets the redirect policy.\n\n  - `:never` (default) Never follow redirects.\n  - `:normal` Always redirect, except from HTTPS URLs to HTTP URLs.\n  - `:always` Always redirect\n\n`priority` an integer between 1 and 256 (both inclusive) for HTTP/2 requests\n\n`proxy` Sets a proxy selector. If not set, uses the default system-wide ProxySelector,\n  which can be configured by Java opts such as `-Dhttp.proxyHost=somehost` and `-Dhttp.proxyPort=80`\n  (see [all options](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/doc-files/net-properties.html#Proxies)).\n  Also accepts:\n\n  - `:no-proxy` to explicitly disable the default behavior, implying a direct connection; or\n  - a [`java.net.ProxySelector`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/ProxySelector.html)\n\n`ssl-context` Sets the SSLContext. If not specified, uses the default `(SSLContext/getDefault)`. Accepts:\n\n  - a map of `:keystore` `:keystore-pass` `:trust-store` `:trust-store-pass`, `:insecure?`. See client authentication examples for more details.\n  - an [`javax.net.ssl.SSLContext`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/javax/net/ssl/SSLContext.html)\n\n`ssl-parameters` a `javax.net.ssl.SSLParameters`\n\n`version` Sets preferred HTTP protocol version.\n  - `:http-1.1` prefer HTTP/1.1\n  - `:http-2` (default) tries to upgrade to HTTP/2, falling back to HTTP/1.1\n\n### Making requests\n\nThe core function for making requests is `hato.client/request`, which takes a [ring](https://github.com/ring-clojure/ring/blob/master/SPEC)\nrequest and returns a response. Convenience wrappers are provided for the http verbs (`get`, `post`, `put` etc.).\n\n```clojure\n; The main request function\n(hc/request {:method :get, :url \"https://httpbin.org/get\"})\n\n; Convenience wrappers\n(hc/get \"https://httpbin.org/get\")\n(hc/get \"https://httpbin.org/get\" {:as :json})\n(hc/post \"https://httpbin.org/post\" {:body \"{\\\"a\\\": 1}\" :content-type :json})\n```\n\n#### request options\n\n`method`Lowercase keyword corresponding to a HTTP request method, such as :get or :post.\n\n`url` An absolute url to the requested resource (e.g. `\"http://moo.com/api/1\"`).\n\n`accept` Sets the `accept` header. a keyword (e.g. `:json`, for any application/* type) or string (e.g. `\"text/html\"`) for anything else.\n\n`accept-encoding` List of string/keywords (e.g. `[:gzip]`). By default, \"gzip, deflate\" will be concatenated\n  unless `decompress-body?` is false.\n\n`content-type` a keyword (e.g. `:json`, for any application/* type) or string (e.g. \"text/html\") for anything else.\n  Sets the appropriate header.\n\n`body` the body of the request. This should be a string, byte array, input stream,\n  or a [`java.net.http.HttpRequest$BodyPublisher`](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpRequest.BodyPublisher.html).\n  To send a clojure map as json (or some other format), use the `form-params` option with the appropriate `content-type`.\n\n`as` Return response body in a certain format. Valid options:\n\n  - Return an object type: `:string` (default), `:byte-array`, `:stream` (_java.lang.io.InputStream_).\n  - `:auto`, to automatically coerce response body based on the response (e.g. `content-type`).\n    This is an alpha feature and the implementation may change.\n  - `:lines`, will give you back a **lazy** `java.util.Stream\u003cString\u003e` (must be closed by the caller).\n    This is very useful for (newline-delimited) server-sent-events (i.e. `text/event-stream` content-type).\n  - Coerce response body with certain format: `:json`, `:json-string-keys`,\n  `:clojure`, `:transit+json`, `:transit+msgpack`. JSON and transit\n  coercion require optional dependencies [cheshire](https://github.com/dakrone/cheshire) (5.9.0 or later) and\n  [com.cognitect/transit-clj](https://github.com/cognitect/transit-clj) to be installed, respectively.\n\n`coerce` Determine which status codes to coerce response bodies. `:unexceptional` (default), `:always`, `:exceptional`.\n  This presently only has an effect for json coercions.\n\n`query-params` A map of options to turn into a query string. See usage examples for details.\n\n`form-params` A map of options that will be sent as the body, depending on the `content-type` option. For example,\n  set `:content-type :json` to coerce the form-params to a json string (requires [cheshire](https://github.com/dakrone/cheshire)).\n  See usage examples for details.\n\n`multi-param-style` Decides how to represent array values when converting `query-params` into a query string. Accepts:\n\n  - When unset (default), a repeating parameter `a=1\u0026a=2\u0026a=3`\n  - `:array`, a repeating param with array suffix: `a[]=1\u0026a[]=2\u0026a[]=3`\n  - `:indexed`, a repeating param with array suffix and index: `a[0]=1\u0026a[1]=2\u0026a[2]=3`\n\n`multipart` A sequence of maps with the following keys:\n\n  - `:name` The name of the param\n  - `:part-name` To preserve the order of entities, `:name` will be used as the part name unless `:part-name` is specified\n  - `:content` The part's data. May be a `String`, `InputStream`, `Reader`, `File`, `char-array`, or a `byte-array`\n  - `:file-name` The part's file name. If the `:content` is a `File`, it will use `.getName` by default but may be overridden.\n  - `:content-type` The part's content type. The value may be a `String` such as `\"text/plain; charset=utf-8\"` or represented as\n    a map such as `{:mime-type \"text/html\"}` or `{:mime-type \"text/plain\" :charset \"iso-8859-1\"}`. If left empty, the value\n    will depend on `:content`. When `:content` is a `String`, it will be `text/plain; charset=UTF-8` and when `:content` \n    is a `File`, it will attempt to guess the best content type or fallback to `application/octet-stream`.\n\n`headers` Map of lower case strings to a header value. A header's value may be a string or a sequence of strings when \n  there are multiple values for a given header.\n\n`basic-auth` Performs basic authentication (sending `Basic` authorization header). Accepts `{:user \"user\" :pass \"pass\"}`\n  Note that basic auth can also be passed via the `url` (e.g. `http://user:pass@moo.com`)\n\n`oauth-token` String, will set `Bearer` authorization header\n\n`decompress-body?` By default, sets request header to accept \"gzip, deflate\" encoding, and decompresses the response.\n  Set to `false` to turn off this behaviour.\n\n`throw-exceptions?` By default, the client will throw exceptions for exceptional response statuses. Set this to\n  `false` to return the response without throwing.\n\n`async?` Boolean, defaults to false. See below section on async requests.\n\n`http-client` An `HttpClient` created by `build-http-client` or other means. For single-use clients, it also\n  accepts a map of the options accepted by `build-http-client`.\n\n`expect-continue` Requests the server to acknowledge the request before sending the body. This is disabled by default.\n\n`timeout` Timeout to receiving a response, in milliseconds (default: unlimited).\n\n`version` Sets preferred HTTP protocol version per request.\n\n  - `:http-1.1` prefer HTTP/1.1\n  - `:http-2` (default) tries to upgrade to HTTP/2, falling back to HTTP/1.1\n\n\n## Usage examples\n\n### Async requests\n\nBy default, hato performs synchronous requests and directly returns a response map.\n\nBy providing `async?` option to the request, the request will be performed asynchronously, returning\na [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html)\nof the response map. This can be wrapped in e.g. [manifold](https://github.com/ztellman/manifold),\nto give you promise chains etc.\n\nAlternatively, callbacks can be used by passing in `respond` and `raise` functions, in which case\nthe [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html)\nreturned can be used to indicate when processing has completed.\n\n```clojure\n; A standard synchronous request\n(hc/get \"https://httpbin.org/get\")\n\n; An async request\n(hc/get \"https://httpbin.org/get\" {:async? true})\n; =\u003e\n; #object[jdk.internal.net.http.common.MinimalFuture...\n\n; Deref it to get the value\n(-\u003e @(hc/get \"https://httpbin.org/get\" {:async? true})\n    :body)\n; =\u003e\n; { ...some json body }\n\n; Pass in a callback\n(hc/get \"https://httpbin.org/get\"\n       { :async? true }\n       (fn [resp] (println \"Got status\" (:status resp)))\n       identity)\n; =\u003e\n; #object[jdk.internal.net.http.common.MinimalFuture...\n; Got status 200\n\n(future-done? *1)\n; =\u003e\n; true\n\n; Exceptional status codes by default will call raise with an ex-info containing the response map.\n; This means we can use ex-data to get the data back out.\n@(hc/get \"https://httpbin.org/status/400\" {:async? true} identity #(-\u003e % ex-data :status))\n; =\u003e\n; 400\n```\n\n### Making queries\n\nhato can generate url encoded query strings in multiple ways\n\n```clojure\n; Via un url\n(hc/get \"http://moo.com?hello=world\u0026a=1\u0026a=2\" {})\n\n; Via query-params\n(hc/get \"http://moo.com\" {:query-params {:hello \"world\" :a [1 2]}})\n\n; Values are urlencoded\n(hc/get \"http://moo.com\" {:query-params {:q \"a-space and-some-chars$\u0026!\"}})\n; Generates query: \"q=a-space+and-some-chars%24%26%21\"\n\n; Nested params are flattened by default\n(hc/get \"http://moo.com\" {:query-params {:a {:b {:c 5} :e {:f 6}}}})\n; =\u003e \"a[b][c]=5\u0026a[e][f]=6\", url encoded\n\n; Flattening can be disabled\n(hc/get \"http://moo.com\" {:query-params {:a {:b {:c 5} :e {:f 6}}} :ignore-nested-query-string true})\n; =\u003e \"a={:b {:c 5}, :e {:f 6}}\", url encoded\n```\n\nForm parameters can also be passed as a map:\n\n```clojure\n(hc/post \"http://moo.com\" {:form-params {:hello \"world\"}})\n\n; Send a json body \"{\\\"a\\\": {\\\"b\\\": 5}}\"\n(hc/post \"http://moo.com\" {:form-params {:a {:b 5}} :content-type :json})\n\n; Nested params are not flattened by default\n; Sends a body of \"a={:b {:c 5}, :e {:f 6}}\", x-www-form-urlencoded\n(hc/post \"http://moo.com\" {:form-params {:a {:b {:c 5} :e {:f 6}}}})\n\n; Flattening can be enabled\n; Sends a body of \"a[b][c]=5\u0026a[e][f]=6\", url encoded\n(hc/post \"http://moo.com\" {:form-params {:a {:b {:c 5} :e {:f 6}}}\n                           :flatten-nested-form-params true})\n```\n\nAs a convenience, nesting can also be controlled by `:flatten-nested-keys`:\n\n```clojure\n; Flattens both query and form params\n(hc/post \"http://moo.com\" {... :flatten-nested-keys [:query-params :form-params]})\n\n; Flattens only query params\n(hc/post \"http://moo.com\" {... :flatten-nested-keys [:query-params]})\n```\n\n\n### Output coercion\n\nYou can control whether you like hato to return an `InputStream` (using `:as :stream`), `byte-array` (using `:as :byte-array`) or `String` (`:as :string`) with no further coercion.\n\n```clojure\n; Returns a string response\n(hc/get \"http://moo.com\" {})\n\n; Returns a byte array\n(hc/get \"http://moo.com\" {:as :byte-array})\n\n; Returns an InputStream\n(hc/get \"http://moo.com\" {:as :stream})\n\n; Coerces clojure strings\n(hc/get \"http://moo.com\" {:as :clojure})\n\n; Coerces transit. Requires optional dependency com.cognitect/transit-clj.\n(hc/get \"http://moo.com\" {:as :transit+json})\n(hc/get \"http://moo.com\" {:as :transit+msgpack})\n\n; Coerces JSON strings into clojure data structure\n; Requires optional dependency cheshire\n(hc/get \"http://moo.com\" {:as :json})\n(hc/get \"http://moo.com\" {:as :json-string-keys})\n\n; Coerce responses with exceptional status codes\n(hc/get \"http://moo.com\" {:as :json :coerce :always})\n```\n\nBy default, hato only coerces JSON responses for unexceptional statuses. Control this with the `:coerce` option:\n\n```clojure\n:unexceptional ; default - only coerce response bodies for unexceptional status codes\n:exceptional ; only coerce for exceptional status codes\n:always ; coerce for any status code\n```\n\n### Certificate authentication\n\nClient authentication can be done by passing in an SSLContext:\n\n```clojure\n; Directly pass in an SSLContext that you made yourself\n(hc/get \"https://secure-url.com\" {:http-client {:ssl-context SomeSSLContext}})\n\n; Pass in your credentials\n(hc/get \"https://secure-url.com\" {:http-client {:ssl-context {:keystore (io/resource \"somepath.p12\")\n                                                              :keystore-pass \"password\"\n                                                              :trust-store (io/resource \"cacerts.p12\"\n                                                              :trust-store-pass \"another-password\")}}})\n\n; Skip verification of the server certificates\n(hc/get \"https://secure-url.com\" {:http-client {:ssl-context {:insecure? true}}})\n```\nIf either `:keystore` or `:trust-store` are not provided, the respective system default will be used.\n\nThe defaults can be overridden with java options, so the below is equivalent to the above (with the caveat\nthat the path should be on the filesystem rather than in the jar resources):\n\n```\n-Djavax.net.ssl.keyStore=somepath.12\n-Djavax.net.ssl.keyStoreType=pkcs12\n-Djavax.net.ssl.keyStorePassword=password\n-Djavax.net.ssl.trustStore=cacerts.p12\n-Djavax.net.ssl.trustStoreType=pkcs12\n-Djavax.net.ssl.trustStorePassword=another-password\n```\n\n### Redirects\n\nBy default, hato does not follow redirects. To change this behaviour, use the `redirect-policy` option.\n\nImplementation notes from the [docs](https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Redirect.html):\n\u003e When automatic redirection occurs, the request method of the redirected request may be modified\ndepending on the specific 30X status code, as specified in [RFC 7231](https://tools.ietf.org/html/rfc7231).\nIn addition, the 301 and 302 status codes cause a POST request to be converted to a GET in the redirected request.\n\n```clojure\n; Always redirect, except from HTTPS URLs to HTTP URLs\n(hc/get \"http://moo.com\" {:http-client {:redirect-policy :normal}})\n\n; Always redirect\n(hc/get \"http://moo.com\" {:http-client {:redirect-policy :always}})\n```\n\nThe Java HttpClient does not provide a direct option for max redirects. By default, it is 5.\nTo change this, set the java option to e.g. `-Djdk.httpclient.redirects.retrylimit=10`.\n\nThe client does not throw an exception if the retry limit has been breached. Instead,\nit will return a response with the redirect status code (30x) and empty body.\n\n### Multipart Requests\n\nTo send a multipart request, `:multipart` may be supplied as a sequence of maps as described in\n[request options](#request-options). This will add the appropriate Content-Type header as well as replace\nthe `:body` of the request with an `InputStream` of the supplied parts.\n\n```clojure\n(hc/post \"http://moo.com\"\n        {:multipart [{:name \"title\" :content \"My Awesome Picture\"}\n                     {:name \"Content/type\" :content \"image/jpeg\"}\n                     {:name \"foo.txt\" :part-name \"eggplant\" :content \"Eggplants\"}\n                     {:name \"file\" :content (io/file \".test-data\")}\n                     {:name \"data\" :content (.getBytes \"hi\" \"UTF-8\") :content-type \"text/plain\" :file-name \"data.txt\"}\n                     {:name \"jsonParam\" :content (io/file \".test-data\") :content-type \"application/json\" :file-name \"data.json\"}]})\n```\n\n\n### Custom middleware\n\nhato has a stack of middleware that it applies by default if you use the built in request function. You can\nsupply different middleware by using `wrap-request` yourself:\n\n```clojure\n; Using the default middleware\n(hc/request {:url \"https://httpbin.org/get\" :method :get})\n\n; With convenience method\n(hc/get \"https://httpbin.org/get\")\n\n; Let's write an access log middleware\n\n; Define a new middleware\n(defn log-and-return\n    [resp]\n    (println :access-log (:uri resp) (:status resp) (:request-time resp))\n    resp)\n\n(defn wrap-log\n  [client]\n  (fn\n    ([req]\n     (let [resp (client req)]\n       (log-and-return resp)))\n    ([req respond raise]\n     (client req\n             #(respond (log-and-return %))\n             raise))))\n\n; Create your own middleware stack.\n; Note that ordering is important here:\n; - After wrap-request-timing so :request-time is available on the response\n; - Before wrap-exceptions so that exceptional responses have not yet caused an exception to be thrown\n(def my-middleware (concat [(first hm/default-middleware) wrap-log] (drop 1 hm/default-middleware)))\n\n; Now it logs\n(request {:url \"https://httpbin.org/get\" :method :get :middleware my-middleware})\n; :access-log https://httpbin.org/get 200 1069\n; =\u003e Returns response map\n\n(request {:url \"https://httpbin.org/status/404\" :method :get :middleware my-middleware})\n; :access-log https://httpbin.org/status/404 404 1924\n; ...Throws some ExceptionInfo\n\n(get \"https://httpbin.org/get\" {:middleware my-middleware})\n; :access-log https://httpbin.org/get 200 1069\n; =\u003e Returns string body\n```\n\n### WebSockets\n\nThe simplest way to get started is with the `websocket` function:\n\n```clojure\n(require '[hato.websocket :as ws])\n\n(let [ws @(ws/websocket \"ws://echo.websocket.events\"\n                        {:on-message (fn [ws msg last?]\n                                       (println \"Received message:\" msg))\n                         :on-close   (fn [ws status reason]\n                                       (println \"WebSocket closed!\"))})]\n  (ws/send! ws \"Hello World!\")\n  (Thread/sleep 1000)\n  (ws/close! ws))\n```\n\nBy default, hato WebSocket functions are asynchronous and most return a\n[CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html). This\ncan be wrapped in e.g. [manifold](https://github.com/ztellman/manifold), to give you promise chains etc.\n\n```clojure\n(require '[hato.websocket :as ws])\n(require '[manifold.deferred :as d])\n\n(-\u003e (ws/websocket \"ws://echo.websocket.events\"\n                  {:on-message (fn [ws msg last?]\n                                 (println \"Received message:\" msg))\n                   :on-close   (fn [ws status reason]\n                                 (println \"WebSocket closed!\"))})\n    (d/chain #(ws/send! % \"Hello\")\n             #(ws/send! % \"World!\")\n             #(ws/close! %))\n    (d/catch Exception #(println \"Something went wrong!\" %)))\n```\n\n### WebSocket options\n\n`uri` A WebSocket uri (e.g. `\"ws://echo.websocket.events\"`).\n\n\n`opts` Additional options may be a map of any of the following keys:\n\n  - `:http-client` An `HttpClient` (e.g. created by `hato.client/build-http-client`). If not provided, a default client will be used.\n  - `:headers` Adds the given name-value pair to the list of additional HTTP headers sent during the opening handshake.\n  - `:connect-timeout` Sets a timeout for establishing a WebSocket connection, in milliseconds.\n  - `:subprotocols` Sets a request for the given subprotocols.\n  - `:listener` A WebSocket listener. If a `WebSocket$Listener` is provided, it will be used directly.\n  Otherwise one will be created from any handlers (`on-\u003cevent\u003e`) passed into the options map.\n\n  - `:on-open` Called when a `WebSocket` has been connected. Called with the WebSocket instance.\n  - `:on-message` A textual/binary data has been received. Called with the WebSocket instance, the data, and whether this invocation completes the message.\n  - `:on-ping` A Ping message has been received. Called with the WebSocket instance and the ping message.\n  - `:on-pong` A Pong message has been received. Called with the WebSocket instance and the pong message.\n  - `:on-close` Receives a Close message indicating the WebSocket's input has been closed. Called with the WebSocket instance, the status code, and the reason.\n  - `:on-error` An error has occurred. Called with the WebSocket instance and the error.\n\n\n### Debugging\n\nTo view the logs of the Java client, add the java option `-Djdk.httpclient.HttpClient.log=all`.\nIn Leiningen, this can be done using `:jvm-opts` in `project.clj`.\n\n### Other advanced options\n\n```\n# Default keep alive for connection pool is 1200 seconds\n-Djdk.httpclient.keepalivetimeout=1200\n\n# Default connection pool size is 0 (unbounded)\n-Djdk.httpclient.connectionPoolSize=0\n```\n\n## License\n\nReleased under the MIT License: http://www.opensource.org/licenses/mit-license.php\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgnarroway%2Fhato","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgnarroway%2Fhato","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgnarroway%2Fhato/lists"}