{"id":15014346,"url":"https://github.com/appsflyer/donkey","last_synced_at":"2025-04-12T22:36:56.634Z","repository":{"id":37931728,"uuid":"299617165","full_name":"AppsFlyer/donkey","owner":"AppsFlyer","description":"Modern Clojure HTTP server and client built for ease of use and performance","archived":false,"fork":false,"pushed_at":"2023-12-05T22:39:57.000Z","size":617,"stargazers_count":293,"open_issues_count":8,"forks_count":17,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-04-04T01:41:26.610Z","etag":null,"topics":["clojure","http-client","http-server","java","netty","ring","vertx"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AppsFlyer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-09-29T12:55:19.000Z","updated_at":"2025-03-18T15:15:39.000Z","dependencies_parsed_at":"2024-01-03T02:24:54.402Z","dependency_job_id":"14df6770-4b40-42fa-99c7-9e12a943466c","html_url":"https://github.com/AppsFlyer/donkey","commit_stats":{"total_commits":189,"total_committers":4,"mean_commits":47.25,"dds":0.05820105820105825,"last_synced_commit":"e36889e3b18da17d151c6a70673653ea4d24c045"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AppsFlyer%2Fdonkey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AppsFlyer%2Fdonkey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AppsFlyer%2Fdonkey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AppsFlyer%2Fdonkey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AppsFlyer","download_url":"https://codeload.github.com/AppsFlyer/donkey/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248642992,"owners_count":21138353,"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":["clojure","http-client","http-server","java","netty","ring","vertx"],"created_at":"2024-09-24T19:45:30.337Z","updated_at":"2025-04-12T22:36:56.608Z","avatar_url":"https://github.com/AppsFlyer.png","language":"Java","readme":"# Donkey\n\n\u003cp align=\"center\"\u003e\u003cimg src=\"https://user-images.githubusercontent.com/29622398/160290393-63e0ccba-d7ab-4853-89d1-7b524ac79178.png\" width=350/\u003e\u003c/p\u003e\n\n![Donkey CI](https://github.com/AppsFlyer/donkey/workflows/Donkey%20CI/badge.svg)\n[![Coverage Status](https://coveralls.io/repos/github/AppsFlyer/donkey/badge.svg?branch=master)](https://coveralls.io/github/AppsFlyer/donkey?branch=master)  \n[![Clojars Project](https://img.shields.io/clojars/v/com.appsflyer/donkey.svg)](https://clojars.org/com.appsflyer/donkey)\n\nModern Clojure, Ring compliant, HTTP server and client, designed for ease of use\nand performance\n\n# Note: this project is no longer maintained.\n\nTable of Contents\n-----------------\n\n* [Usage](#usage)\n* [Requirements](#requirements)\n* [Building](#building)\n* [Start up options](#start-up-options)\n* [Creating a Donkey](#creating-a-donkey)\n* [Server](#server)\n    * [Creating a Server](#creating-a-server)\n    * [Routes](#routes)\n    * [Support for Routing Libraries](#support-for-routing-libraries)\n        * [reitit](#reitit)\n        * [Compojure](#compojure)\n    * [Static Resources](#static-resources)\n    * [Middleware](#middleware)\n        * [Overview](#overview)\n        * [Examples](#examples)\n        * [Common Middleware](#common-middleware)\n    * [Examples](#server-examples)\n* [Client](#client)\n    * [Creating a Client](#creating-a-client)\n    * [Stopping a Client](#stopping-a-client)\n    * [Submitting a Request](#submitting-a-request)\n    * [FutureResult](#futureresult)\n    * [HTTPS Requests](#https-requests)\n* [Metrics](#metrics)\n    * [List of Exposed Metrics](#list-of-exposed-metrics)\n        * [Thread Pool Metrics](#thread-pool-metrics)\n        * [Server Metrics](#server-metrics)\n        * [Client Metrics](#client-metrics)\n* [Debug mode](#debug-mode)\n    * [Logging](#logging)\n* [Troubleshooting](#troubleshooting)\n* [License](#license)\n\nTOC Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)\n\n### Usage\n\nIncluding the library in `project.clj`\n\n```clojure\n[com.appsflyer/donkey \"0.5.2\"]\n``` \n\nIncluding the library in `deps.edn`\n\n```clojure\ncom.appsflyer/donkey {:mvn/version \"0.5.2\"}\n``` \n\nIncluding the library in `pom.xml`\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.appsflyer\u003c/groupId\u003e\n    \u003cartifactId\u003edonkey\u003c/artifactId\u003e\n    \u003cversion\u003e0.5.2\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Requirements\n\n- [Java](https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html)\n  11+\n- [Maven](http://maven.apache.org/download.cgi) 3.6.3+\n- [Leiningen](https://leiningen.org/) 2.9.3+\n\n### Building\n\nThe preferred way to build the project for local development is using Maven.\nIt's also possible to generate an uberjar using Leiningen, but you\n**must** use Maven to install the library locally.\n\nCreating a jar with Maven\n\n```shell script\nmvn package\n```\n\nCreating an uberjar with Leiningen\n\n```shell script\nlein uberjar\n```\n\nInstalling to a local repository\n\n```shell script\nmvn clean install\n```   \n\n## Start up options\n\nJVM system properties that can be supplied when running the application\n\n- `-Dvertx.threadChecks=false`: Disable blocked thread checks. Used by Vert.x to\n  warn the user if an event loop or worker thread is being occupied above a\n  certain threshold which will indicate the code should be examined.\n- `-Dvertx.disableContextTimings=true`: Disable timing context execution. These\n  are used by the blocked thread checker. It does _**not**_ disable execution\n  metrics that are exposed via JMX.\n\n## Creating a Donkey\n\nIn Donkey, you create HTTP servers and clients using a - `Donkey`. Creating\na `Donkey` is simple:\n\n```clojure\n(ns com.appsflyer.sample-app\n  (:require [com.appsflyer.donkey.core :refer [create-donkey]]))\n\n  (def ^Donkey donkey-core (create-donkey))\n```  \n\nWe can also configure our donkey instance:\n\n```clojure\n(ns com.appsflyer.sample-app\n  (:require [com.appsflyer.donkey.core :refer [create-donkey]]))\n\n  (def donkey-core (create-donkey {:event-loops 4}))\n```\n\nThere should only be a single `Donkey` instance per application. That's because\nthe client and server will share the same resources making them very efficient.\n`Donkey` is a factory for creating server(s) and client(s) (you _can_ create\nmultiple servers and clients with a `Donkey`, but in almost all cases you will\nonly want a single server and / or client per application).\n\n## Server\n\nThe following examples assume these required namespaces\n\n```clojure\n(:require [com.appsflyer.donkey.core :refer [create-donkey create-server]]\n          [com.appsflyer.donkey.server :refer [start]]\n          [com.appsflyer.donkey.result :refer [on-success]])\n```\n\n### Creating a Server\n\nCreating a server is done using a `Donkey` instance. Let's start by creating a\nserver listening for requests on port 8080.\n\n```clojure\n(-\u003e         \n  (create-donkey)\n  (create-server {:port 8080})\n  start\n  (on-success (fn [_] (println \"Server started listening on port 8080\"))))\n``` \n\n_Note that the following example will not work yet - for it to work we need to\nadd a route which we will do next._\n\nAfter creating the server we `start` it, which is an asynchronous call that may\nreturn before the server actually started listening for incoming connections.\nIt's possible to block the current thread execution until the server is running\nby calling `start-sync` or by \"derefing\" the arrow macro.\n\nThe next thing we need to do is define a route. We talk about [routes](#routes)\nin depth later on, but a route is basically a definition of an endpoint. Let's\ndefine a route and create a basic \"Hello world\" endpoint.\n\n```clojure\n(-\u003e\n  (create-donkey)\n  (create-server {:port   8080\n                  :routes [{:handler (fn [_request respond _raise]\n                                       (respond {:body \"Hello, world!\"}))}]})\n  start\n  (on-success (fn [_] (println \"Server started listening on port 8080\"))))\n``` \n\nAs you can see we added a `:routes` key to the options map used to initialize\nthe server. A route is a map that describes what kind of requests are handled at\na specific resource address (or `:path`), and how to handle them. The only\nrequired key is `:handler`, which will be called when a request matches a route.\nIn the example above we're saying that we would like any request to be handled\nby our handler function.\n\nOur handler is a Ring compliant asynchronous handler. If you are not familiar\nwith the [Ring](https://github.com/ring-clojure/ring/blob/master/SPEC)\nasync handler specification, here's an excerpt:\n\u003e An asynchronous handler takes 3 arguments: a request map, a callback function for sending a response, and a callback function for raising an exception. The response callback takes a response map as its argument. The exception callback takes an exception as its argument.\n\nIn the handler we are calling the response callback `respond` with a response\nmap where the body of the response is \"Hello, world!\".\n\nIf you run the example and open a browser on `http://localhost:8080` you will\nsee a page with \"Hello, World!\".\n\n### Routes\n\nIn Donkey HTTP requests are routed to handlers. When you initialize a server you\ndefine a set of routes that it should handle. When a request arrives the server\nchecks if one of the routes can handle the request. If no matching route is\nfound, then a `404 Not Found` response is returned to the client.\n\nLet's see a route example:\n\n```clojure\n{\n  :handler      (fn [request respond raise] ...)\n  :handler-mode :non-blocking\n  :path         \"/api/v2\"\n  :match-type   :simple\n  :methods      [:get :put :post :delete]\n  :consumes     [\"application/json\"]\n  :produces     [\"application/json\"]\n  :middleware   [(fn [handler] (fn [request respond raise] (handler request respond raise)))]\n}\n```   \n\n`:handler` A function that accepts 1 or 3 arguments (depending on\n`:handler-mode`). The function will be called if a request matches the route.\nThis is where you call your application code. The handler should return a\nresponse map with the following optional fields:\n\n- `:status`: The response status code (defaults to 200)\n- `:headers`: Map of key -\u003e value `String` pairs\n- `:body`: The response body as `byte[]`, `String`, or `InputStream`\n\n`:handler-mode` To better understand the use of the `:handler-mode`, we need to\nfirst get some background about Donkey. Donkey is an abstraction built on top of\na web tool-kit called [Vert.x](https://vertx.io/), which in turn is built on a\nvery popular and performant networking library called\n[Netty](https://netty.io/). Netty's architecture is based on the concept of a\nsingle threaded event loop that serves requests. An event loop is conceptually a\nlong-running task with a queue of events it needs to dispatch. As long as events\nare dispatched \"quickly\" and don't occupy too much of the event loop's time, it\ncan dispatch events at a very high rate. Because it is single threaded, or in\nother words serial, during the time it takes to dispatch one event no other\nevent can be dispatched. Therefore, it's extremely important *not to block the\nevent loop*.\n\nThe `:handler-mode` is a contract where you declare the type of handling your\nroute does - `:blocking` or `:non-blocking` (default).\n`:non-blocking` means that the handler is performing very fast CPU-bound tasks,\nor non-blocking IO bound tasks. In both cases the guarantee is that it will *not\nblock the event loop*. In this case the `:handler` must accept 3 arguments.\nSometimes reality has it that we have to deal with legacy code that is doing\nsome blocking operations that we just cannot change easily. For these occasions\nwe have `:blocking` handler mode. In this case, the handler will be called on a\nseparate worker thread pool without needing to worry about blocking the event\nloop. The worker thread pool size can be configured when creating a\n[`Donkey`](#creating-a-donkey) instance by setting the `:worker-threads` option.\n\n`:path` is the first thing a route is matched on. It is the part after the\nhostname in a URI that identifies a resource on the host the client is trying to\naccess. The way the path is matched depends on the `:match-type`.\n\n`:match-type` can be either `:simple` or `:regex`.\n\n`:simple` match type will match in two ways:\n\n1) **Exact match**. Going back to the example route at the begining of the\n   section, the route will only match requests to `http://localhost:8080/api/v2`\n   . It will _not_ match requests to:\n    - `http://localhost:8080/api`\n    - `http://localhost:8080/api/v3`\n    - `http://localhost:8080/api/v2/user`\n2) **Path variables**. Take for example the path `/api/v2/user/:id/address`\n   . `:id`\n   is a path variable that matches on any sub-path. All the following paths will\n   match:\n    - `/api/v2/user/1035/address`\n    - `/api/v2/user/2/address`\n    - `/api/v2/user/foo/address`   \n      The really nice thing about path variables is that you get the value that\n      was in the path when it matched, in the request. The value will be\n      available in the\n      `:path-params` map. If we take the first example, the request will look\n      like this:\n\n```clojure\n{\n;; ...\n  :path-params {\"id\" \"1035\"}\n;; ... \n}\n```        \n\n`:regex` match type will match on arbitrary regular expressions. For example, if\nwanted to only match the `/api/v2/user/:id/address` path if `:id` is a number,\nthen we could use `:match-type :regex` and supply this path:\n`/api/v2/user/[0-9]+/address`. In this case the route will only match if a\nclient requests the path with a numeric id, but we won't have access to the id\nin the `:path-params` map. If we wanted the id we could fix it by adding\ncapturing groups: `/api/v2/user/([0-9]+)/address`. Now everything within the\nparenthesis will be available in `:path-params`.\n\n```clojure\n{\n  :path-params {\"param0\" \"1035\"}\n}\n```\n\nWe can also add multiple capturing groups, for example the path\n`/api/v(\\d+\\.\\d{1})/user/([0-9]+)/address` will match `/api/v4.7/user/9/address`\nand `:path-params` will include both capturing groups.\n\n```clojure\n{\n  :path-params {\"param0\" \"4.7\" \n                \"param1\" \"9\"}\n}\n```\n\n`:methods` is a vector of HTTP methods the route supports, such as GET, POST,\netc'. By default, any method will match the route.\n\n`:consumes` is a vector of media types that the handler can consume. If a route\nmatches but the `Content-Type` header of the request doesn't match one of the\nsupported media types, then the request will be rejected with a\n`415 Unsupported Media Type` code.\n\n`:produces` is a vector of media types that the handler produces. If a route\nmatches but the `Accept` header of the request doesn't match one of the\nsupported media types, then the request will be rejected with a\n`406 Not Acceptable` code.\n\n`:middleware` is a vector of [middleware](#middleware) functions that will be\napplied to the route. It is also possible to supply a \"global\"\n`:middleware` vector when [creating a server](#creating-a-server) that will be\napplied to all the routes. In that case the global middleware will be applied\n*first*, followed by the middleware specific to the route.\n\n### Support for Routing Libraries\n\nSometimes we have an existing service using some HTTP server and routing\nlibraries such as [Compojure](https://github.com/weavejester/compojure)\nor [reitit](https://github.com/metosin/reitit), and we don't have time to\nrewrite the routing logic right away. It's very easy to simply plug all your\nexisting routing logic to Donkey without changing a line of code.\n\nWe'll use Compojure and reitit as examples, but the same goes for any other Ring\ncompatible library you use.\n\n#### reitit\n\nHere is an excerpt from Metosin's reitit\n[Ring-router](https://cljdoc.org/d/metosin/reitit/0.5.5/doc/introduction#ring-router)\ndocumentation, demonstrating how to create a simple router.\n\n```clojure\n(require '[reitit.ring :as ring])\n\n(defn handler [_]\n  {:status 200, :body \"ok\"})\n\n(defn wrap [handler id]\n  (fn [request]\n    (update (handler request) :wrap (fnil conj '()) id)))\n\n(def app\n  (ring/ring-handler\n    (ring/router\n      [\"/api\" {:middleware [[wrap :api]]}\n       [\"/ping\" {:get handler\n                 :name ::ping}]\n       [\"/admin\" {:middleware [[wrap :admin]]}\n        [\"/users\" {:get handler\n                   :post handler}]]])))\n```\n\nNow let's see how you would use this router with Donkey.\n\n```clojure\n(-\u003e \n  (create-donkey)\n  (create-server {:port 8080 \n                  :routes [{:handler app \n                            :handler-mode :blocking}]})\n  start)\n```  \n\nThat's it!\n\nBasically, we're creating a single route that will match any request to the\nserver and will delegate the routing logic and request handling to the reitit\nrouter. You'll notice we had to add `:handler-mode :blocking` to the route.\nThat's because this particular example uses the one argument ring handler. If we\nadd a three argument arity to `handler` and `wrap`, then we'll be able to remove\n`:handler-mode :blocking` and use the default non-blocking mode.\n\n#### Compojure\n\nHere is an excerpt from James Reeves'\n[Compojure](https://github.com/weavejester/compojure) repository on GitHub,\ndemonstrating how to create a simple router.\n\n```clojure\n(ns hello-world.core\n  (:require [compojure.core :refer :all]\n            [compojure.route :as route]))\n\n(defroutes app\n  (GET \"/\" [] \"\u003ch1\u003eHello World\u003c/h1\u003e\")\n  (route/not-found \"\u003ch1\u003ePage not found\u003c/h1\u003e\"))\n```\n\nTo use this router with Donkey we do exactly the same thing we did for\n[reitit](#reitit)'s router.\n\n```clojure\n(-\u003e \n  (create-donkey)\n  (create-server {:port 8080 \n                  :routes [{:handler app \n                            :handler-mode :blocking}]})\n  start)\n```   \n\n### Static Resources\n\nEvery server needs to be able to serve static resources such as HTML,\nJavaScript, or image files. In Donkey, you configure how to serve static files\nby providing a `:resources` map when creating the server. An example is worth a\nthousand words:\n\n```clojure\n:resources {:enable-caching               true\n            :max-age-seconds              1800\n            :local-cache-duration-seconds 60\n            :local-cache-size             1000\n            :resources-root               \"public\"\n            :index-page                   \"home.html\"\n            :routes                       [{:path \"/\"}\n                                           {:path \"/js/.+\\.min\\.js\"}\n                                           {:path \"/images/.+\"\n                                           :produces [\"image/*\"]}]}\n```\n\nThe configuration enables cache handling via the `Cache-Control` header, and\ndefines when cached resources become stale. The `:index-page` tells the server\nwhich file to serve when a directory is requested, and the `resources-root` is\nthe directory where all assets reside.\n\nNow let's take a look at the `:routes` vector that defines the paths where\ndifferent resources are located. The first route defines the file that's served\nwhen requesting the root directory of the site. For example, if our site's\nhostname is `example.com`, then when the server gets a request\nfor `http://example.com` or `http://example.com/`\nit will serve the index page `home.html`. The file is served from\n`\u003cpath to resources directory\u003e/public/home.html`\n\nThe second and third routes use regular expressions to define which files should\nbe served from the `js` and `image` directories. here is an example of a request\nfor a JavaScript file:\n\n`http://example.com/js/app.min.js`\n\nIn this example, if the unminified files are requested the route won't match:\n\n`http://example.com/js/app.js ;; will return 404 not found`\n\nThe third route defines where images are served from, and it also declares that\nit will only serve files with mime type `image/*`. If the request's\n`Accept` header doesn't match an image mime type, then the request will be\nrejected with a `406 Not Acceptable` code.\n\n### Middleware\n\n#### Overview\n\nThe term \"middleware\" is generally used in the context of HTTP frameworks as a\npluggable unit of functionality that can examine or manipulate the flow of bytes\nbetween a client and a server. In other words, it allows users to do things such\nas logging, compression, validation, authorization, and transformation (to name\na few)\nof requests and responses.\n\nAccording to\nthe [Ring](https://github.com/ring-clojure/ring/wiki/Concepts#middleware)\nspecification, middleware are implemented\nas [higher-order functions](https://clojure.org/guides/higher_order_functions)\nthat accept one or more arguments, where the first argument is the\nnext `handler` function, and any optional arguments required by the middleware.\nA `handler` in this context can be either another middleware, or\na [route](#routes) handler. The higher-order function should return a function\nthat accepts one or three arguments:\n\n- One argument: Called when `:handler-mode` is `:blocking` with a `request` map.\n- Three arguments: Called when `:handler-mode` is `:non-blocking` with a\n  `request` map, `respond` function, and `raise` function. The `respond`\n  function should be called with the result of the next handler, and the `raise`\n  function should be called when it is impossible to continue processing the\n  request because of an exception.\n\nThe `handler` argument that was given to the higher-order function has the same\nsignature as the function being returned. It is the middleware author's\nresponsibility to call the next `handler` at some point.\n\n#### Examples\n\nLet's start with a middleware that adds a timestamp to a request. It can be\ncalled with `:handler-mode` `:blocking` or `non-blocking`:\n\n```clojure\n(defn add-timestamp-middleware [handler]\n  (fn\n    ([request]\n      (handler\n        (assoc request :timestamp (System/currentTimeMillis))))\n    ([request respond raise]\n     (try\n       (handler\n         (assoc request :timestamp (System/currentTimeMillis)) respond raise)\n       (catch Exception ex\n         (raise ex))))))\n```\n\nIn the last example we updated the request and called the next handler with the\ntransformed request. However, middleware is not limited to only processing and\ntransforming the request. Here is an example of a three argument middleware that\nadds a `Content-Type` header to the _response_.\n\n```clojure\n(defn add-content-type-middleware [handler]\n  (fn [request respond raise]\n    (let [respond' (fn [response]\n                     (try\n                       (respond\n                         (update response :headers assoc \"Content-Type\" \"text/plain\"))\n                       (catch Exception ex\n                         (raise ex))))]\n        \n      (handler request respond' raise))))\n```\n\nAs mentioned before, the three argument function is called when the\n`:handler-mode` is `:non-blocking`. Notice that we are doing the processing on\nthe calling thread - the event loop. That's because the overhead of\n[context switching](https://www.tutorialspoint.com/what-is-context-switching-in-operating-system)\nand potentially spawning a new thread by offloading a simple `assoc`\nor `update` to a separate thread pool would greatly outweigh the processing time\non the event loop. However, if for example we had a middleware that performs\nsome blocking operation on a remote database, then we would need to run it on a\nseparate thread.\n\nIn this example we authenticate a user with a remote service. For the sake of\nthe example, all we need to know is that we get back a\n[CompletableFuture](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CompletableFuture.html)\nthat is executed on a different thread. When the future completes, we check if\nwe had an exception, and then either call the next `handler` with the updated\nrequest, or stop the execution by calling `raise`.\n\n```clojure\n(defn user-authentication-middleware [handler]\n  (fn [request respond raise]\n    (.whenComplete\n      ^CompletableFuture (authenticate-user request)\n      (reify BiConsumer\n        (accept [this result exception]\n          (if (nil? exception)\n            (handler (assoc request :authenticated result) respond raise)\n            (raise exception)))))))\n```\n\n#### Common Middleware\n\nThere are some common operations that Donkey provides as pre-made middleware\nthat can be found under `com.appsflyer.donkey.middleware.*` namespaces. All the\nmiddleware that come with Donkey take an optional options map. The options map\ncan be used, for example, to supply an exception handler.\n\nA very common use case is inspecting the query parameters sent by a client in\nthe url of a GET request. By default, the query parameters are available in the\nrequest as a string under `:query-string`. It would be much more useful if we\nalso had a map of name value pairs we can easily use.\n\n```clojure\n(:require [com.appsflyer.donkey.middleware.params :refer [parse-query-params]])\n\n(-\u003e\n  (create-donkey)\n  (create-server {:port   8080\n                  :routes [{:path       \"/greet\"\n                            :methods    [:get]    \n                            :handler    (fn [req res _err]\n                                          (res {:body (str \"Hello, \"\n                                                           (get-in req [:query-params \"fname\"])\n                                                           \" \"\n                                                           (get-in req [:query-params \"lname\"]))}))\n                            :middleware [(parse-query-params)]}]})\n  start)\n```\n\nIn this example we are using the `parse-query-params` middleware, that does\nexactly that. Now if we make a `GET` request\n`http://localhost:8080/greet?fname=foo\u0026lname=bar` we'll get back:\n\u003e Hello, foo bar\n\nAnother common use case is converting the names of each query parameter into a\nkeyword. We can achieve both objectives with one middleware:\n\n```clojure\n(:require [com.appsflyer.donkey.middleware.params :refer [parse-query-params]])\n\n(-\u003e\n  (create-donkey)\n  (create-server {:port   8080\n                  :routes [{:path       \"/greet\"\n                            :methods    [:get]    \n                            :handler    (fn [req res _err]\n                                          (res {:body (str \"Hello, \"\n                                                           (-\u003e req :query-params :fname)\n                                                           \" \"\n                                                           (-\u003e req :query-params :lname))}))\n                            :middleware [(parse-query-params {:keywordize true})]}]})\n  start)\n```\n\n### Server Examples\n\nConsumes \u0026 Produces (see [Routes](#routes) section)\n\n```clojure\n(-\u003e\n  (donkey/create-donkey)\n  (donkey/create-server\n    {:port   8080\n     :routes [{:path         \"/hello-world\"\n               :methods      [:get]\n               :handler-mode :blocking\n               :consumes     [\"text/plain\"]\n               :produces     [\"application/json\"]\n               :handler      (fn [request]\n                               {:status 200\n                                :body   \"{\\\"greet\\\":\\\"Hello world!\\\"}\"})}]})\n  server/start)\n```\n\nPath variables (see [Routes](#routes) section)\n\n```clojure\n(-\u003e\n  (donkey/create-donkey)\n  (donkey/create-server\n    {:port   8080\n     :routes [{:path     \"/greet/:name\"\n               :methods  [:get]\n               :consumes [\"text/plain\"]\n               :handler  (fn [req respond _raise]\n                           (respond\n                             {:status  200\n                              :headers {\"content-type\" \"text/plain\"}\n                              :body    (str \"Hello \" (-\u003e :path-params req (get \"name\")))}))}]})\n  server/start)\n```\n\n## Client\n\nThe following examples assume these required namespaces\n\n```clojure\n(:require [com.appsflyer.donkey.core :as donkey]\n          [com.appsflyer.donkey.client :refer [request stop]]\n          [com.appsflyer.donkey.result :refer [on-complete on-success on-fail]]\n          [com.appsflyer.donkey.request :refer [submit submit-form submit-multipart-form]])\n```\n\n### Creating a Client\n\nCreating a client is as simple as this\n\n```clojure\n(let [donkey-client (-\u003e\n                      (donkey/create-donkey) \n                      donkey/create-client)])\n```\n\nWe can set up the client with some default options, so we won't need to supply\nthem on every request\n\n```clojure\n(let [donkey-client (-\u003e\n                      (donkey/create-donkey) \n                      donkey/create-client \n                      {:default-host               \"reqres.in\"\n                       :default-port               443\n                       :ssl                        true\n                       :keep-alive                 true\n                       :keep-alive-timeout-seconds 30\n                       :connect-timeout-seconds    10\n                       :idle-timeout-seconds       20\n                       :enable-user-agent          true\n                       :user-agent                 \"Donkey Server\"\n                       :compression                true})]\n    (-\u003e donkey-client\n        (request {:method :get\n                  :uri    \"/api/users\"})\n        submit\n        (on-complete \n          (fn [res ex] \n            (println (if ex \"Failed!\" \"Success!\"))))))\n```\n\nThe previous example made an HTTPS request to some REST api and printed out\n\"Failed!\" if an exception was received, or \"Success!\" if we got a response from\nthe server. We'll discuss how submitting requests and handling responses work\nshortly.\n\n### Stopping a Client\n\nOnce we're done with a client we should always stop it. This will release all\nthe resources being held by the client, such as connections, event loops, etc'.\nYou should reuse a single client throughout the lifetime of the application, and\nstop it only if it won't be used again. Once stopped it should not be used\nagain.\n\n```clojure\n(stop donkey-client)\n```\n\n### Creating a Request\n\nWhen creating a request we supply an options map that defines it. The map has to\ncontain a `:method` key, and either an `:uri` or an `:url`. The `:uri` key\ndefines the location of the resource being requested, for example:\n\n```clojure\n(-\u003e \n  donkey-client\n  (request {:method :get\n            :uri    \"/api/v1/users\"}))\n```\n\nThe `:url` key defines the absolute URL of the resource, for example:\n\n```clojure\n(-\u003e \n  donkey-client\n  (request {:method :get\n            :url    \"http://www.example.com/api/v1/users\"}))\n```\n\nWhen an `:url` is supplied then the `:uri`, `:port`, `:host` and `:ssl`\nkeys are ignored.\n\n### Submitting a Request\n\nCalling `(def async-request (request donkey-client opts))` creates an\n`AsyncRequest` but does not submit the request yet. You can reuse an\n`AsyncRequest` instance to make the same request multiple times. There are\nseveral ways a request can be submitted:\n\n- `(submit async-request)` submits a request without a body. This is usually the\n  case when doing a `GET` request.\n- `(submit async-request body)` submits a request with a raw body. `body` can be\n  either a string, or a byte array. A typical use case would be `POST`ing\n  serialized data such as JSON. Another common use case is sending binary data\n  by also adding a `Content-Type: application/octet-stream` header to the\n  request.\n- `(submit-form async-request body)` submits an urlencoded form. A\n  `Content-Type: application/x-www-form-urlencoded` header will be added to the\n  request, and the body will be urlencoded. `body` is a map of string key-value\n  pairs. For example, this is how you would typically submit a sign in form on a\n  website:\n\n```clojure\n(submit-form async-request {\"email\"    \"frankies15@example.com\" \n                            \"password\" \"password\"})\n```\n\n- `(submit-multipart-form async-request body)` submits a multipart form. A\n  `Content-Type: multipart/form-data` header will be added to the request.\n  Multipart forms can be used to send simple key-value attribute pairs, and\n  uploading files. For example, you can upload a file from the filesystem along\n  with some attributes like this:\n\n```clojure\n(submit-multipart-form \n  async-request \n    {\"Lyrics\"     \"Phil Silvers\"\n     \"Music\"      \"Jimmy Van Heusen\"\n     \"Title\"      \"Nancy (with the Laughing Face)\"\n     \"Media Type\" \"MP3\"\n     \"Media\"      {\n                   \"filename\"       \"nancy.mp3\"\n                   \"pathname\"       \"/home/bill/Music/Sinatra/Best of Columbia/nancy.mp3\"\n                   \"media-type\"     \"audio/mpeg\"\n                   \"upload-as\"      \"binary\"}})\n```  \n\n### FutureResult\n\nRequests are submitted asynchronously, meaning the request is executed on a\nbackground thread, and calls to `submit[-xxx]*` return a `FutureResult`\nimmediately. You can think of a `FutureResult` as a way to subscribe to an event\nthat may have happened or will happen some time in the future. The api is very\nsimple:\n\n- `(on-success async-result (fn [result]))` will call the supplied function with\n  a response map from the server, iff there were no client side errors while\n  executing the request. Client side errors include an unhandled exception, or\n  problems connecting with the server. It does not include server errors such as\n  4xx or 5xx response status codes. The response will have the usual Ring fields\n  `:status`, `:body`, and optional `:headers`.\n- `(on-fail async-result (fn [ex]))` will call the supplied function with\n  an `ExceptionInfo` indicating the request failed due to a client error.\n- `(on-complete async-result (fn [result ex]))` will always call the supplied\n  function whether the request was successful or not. A successful request will\n  be called with `ex` being `nil`, and a failed request will be called\n  with `result` being `nil`. The two are mutually exclusive which makes it\n  simple to check the outcome of the request.\n\nIf the response is irrelevant as is the case in \"call and forget\" type requests,\nthen the result can be ignored:\n\n```clojure\n(submit async-request) ; =\u003e The `FutureResult` returned is ignored\n... do the rest of your application logic\n```\n\nOr if you are only interested to know if the request failed:\n\n```clojure\n(-\u003e \n  (submit async-request)\n  (on-fail (fn [ex] (println (str \"Oh, no. That was not expected - \" (ex-message ex)))))\n... do the rest of your application logic\n```\n\nAlthough it is not recommended in the context of asynchronous operations,\nresults can also be dereferenced:\n\n```clojure\n(let [result @(submit async-request)]\n  (if (map? result)\n    (println \"Yea!\")\n    (println \"Nay :(\")))\n``` \n\nIn this case the call to `submit` will block the calling thread until a result\nis available. The result may be either a response map, if the request was\nsuccessful, or an `ExceptionInfo` if it wasn't.\n\nEach function returns a new `FutureResult` instance, which makes it possible to\nchain handlers. Let's look at an example:\n\n```clojure\n(ns com.appsflyer.donkey.exmaple\n  (:require [com.appsflyer.donkey.result :as result])\n  (:import (com.appsflyer.donkey FutureResult)))\n\n; Chaning example. Each function gets the return value of the previous\n\n(letfn [(increment [val]\n                  (let [res (update val :count (fnil inc 0))]\n                    (println res)\n                    res))]\n  (-\u003e\n    (FutureResult/create {})\n    (result/on-success increment)\n    (result/on-success increment)\n    (result/on-success increment)\n    (result/on-fail (fn [_ex] (println \"We have a problem\"))))\n\n; Output:\n; {:count 1}\n; {:count 2}\n; {:count 3}\n```\n\nWe start off by defining an `increment` function that takes a map and increments\na `:counter` key. We then create a `FutureResult` that completes with an empty\nmap. The first example shows how chaining the result of one function to the next\nworks.\n\n---\n\nThe rest of the examples assume the following vars are defined\n\n```clojure\n(def donkey-core (donkey/create-donkey))\n(def donkey-client (donkey/create-client donkey-core)\n```  \n\n### HTTPS Requests\n\nMaking HTTPS requests requires setting `:ssl` to `true` and `:default-port` or\n`:port` when creating a client or a request respectively.\n\n```clojure\n(-\u003e\n  (request donkey-client {:host   \"reqres.in\"\n                          :port   443\n                          :ssl    true\n                          :uri    \"/api/users?page=2\"\n                          :method :get})\n  submit\n  (on-success (fn [res] (println res)))\n  (on-fail (fn [ex] (println ex))))\n\n ;  Will output something like this:\n ; `{:status 200, \n     :headers {Age 365, Access-Control-Allow-Origin *, CF-Cache-Status HIT, Via 1.1 vegur, Set-Cookie __cfduid=1234.abcd; expires=Mon, 12-Oct-20 14:50:48 GMT; path=/; domain=.reqres.in; HttpOnly; SameSite=Lax; Secure, Date Sat, 12 Sep 2020 14:50:48 GMT, Accept-Ranges bytes, cf-request-id 0909abcd, Expect-CT max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\", Cache-Control max-age=14400, Content-Length 1245, Server cloudflare, Content-Type application/json; charset=utf-8, Connection keep-alive, Etag W/\"4dd-IPv5LdOOb6s5S9E3i59wBCJ1k/0\", X-Powered-By Express, CF-RAY 5d1a7165fa2cad73-TLV}, \n     :body #object[[B 0x7be7d50c [B@7be7d50c]}`\n\n```\n\n## Metrics\n\nThe library uses [Dropwizard](https://metrics.dropwizard.io/4.1.2/) to capture\ndifferent metrics. The metrics can be largely grouped into three categories:\n\n- Thread Pool\n- Server\n- Client\n\nMetrics collection can be set up when creating a `Donkey` by supplying a pre\ninstantiated instance of `MetricRegistry`. It's the user's responsibility to\nimplement reporting to a monitoring backend such\nas [Prometheus](https://prometheus.io/), or [graphite](https://graphiteapp.org/)\n. As later described, metrics are named using a dot `.` separator. By default,\nall metrics are prefixed with `donkey`, but it's also possible to supply\na `:metrics-prefix` with the `:metric-registry`\nto use a different string.\n\n### List of Exposed Metrics\n\n#### Thread Pool Metrics\n\nBase name:  `\u003c:metrics-prefix\u003e`\n\n- `event-loop-size` - A Gauge of the number of threads in the event loop pool\n- `worker-pool-size` - A Gauge of the number of threads in the worker pool\n\nBase name:  `\u003c:metrics-prefix\u003e.pools.worker.vert.x-worker-thread`\n\n- `queue-delay` - A Timer measuring the duration of the delay to obtain the\n  resource, i.e. the wait time in the queue\n- `queue-size` - A Counter of the actual number of waiters in the queue\n- `usage` - A Timer measuring the duration of the usage of the resource\n- `in-use` - A count of the actual number of resources used\n- `pool-ratio` - A ratio Gauge of the in use resource / pool size\n- `max-pool-size` - A Gauge of the max pool size\n\n#### Server Metrics\n\nBase name: `\u003c:metrics-prefix\u003e.http.servers.\u003chost\u003e:\u003cport\u003e`\n\n- `open-netsockets` - A Counter of the number of open net socket connections\n- `open-netsockets.\u003cremote-host\u003e` - A Counter of the number of open net socket\n  connections for a particular remote host\n- `connections` - A Timer of a connection and the rate of its occurrence\n- `exceptions` - A Counter of the number of exceptions\n- `bytes-read` - A Histogram of the number of bytes read.\n- `bytes-written` - A Histogram of the number of bytes written.\n- `requests` - A Throughput Timer of a request and the rate of it’s occurrence\n- `\u003chttp-method\u003e-requests` - A Throughput Timer of a specific HTTP method\n  request, and the rate of its occurrence. Examples: get-requests, post-requests\n- `responses-1xx` - A ThroughputMeter of the 1xx response code\n- `responses-2xx` - A ThroughputMeter of the 2xx response code\n- `responses-3xx` - A ThroughputMeter of the 3xx response code\n- `responses-4xx` - A ThroughputMeter of the 4xx response code\n- `responses-5xx` - A ThroughputMeter of the 5xx response code\n\n#### Client Metrics\n\nBase name: `\u003c:metrics-prefix\u003e.http.clients`\n\n- `open-netsockets` - A Counter of the number of open net socket connections\n- `open-netsockets.\u003cremote-host\u003e` - A Counter of the number of open net socket\n  connections for a particular remote host\n- `connections` - A Timer of a connection and the rate of its occurrence\n- `exceptions` - A Counter of the number of exceptions\n- `bytes-read` - A Histogram of the number of bytes read.\n- `bytes-written` - A Histogram of the number of bytes written.\n- `connections.max-pool-size` - A Gauge of the max connection pool size\n- `connections.pool-ratio` - A ratio Gauge of the open connections / max\n  connection pool size\n- `responses-1xx` - A Meter of the 1xx response code\n- `responses-2xx` - A Meter of the 2xx response code\n- `responses-3xx` - A Meter of the 3xx response code\n- `responses-4xx` - A Meter of the 4xx response code\n- `responses-5xx` - A Meter of the 5xx response code\n\n## Debug mode\n\nDebug mode is activated when creating a `Donkey` with `:debug true`. In this\nmode several loggers are set to log at the `trace` level. It means the logs will\nbe *very* verbose. For that reason it is not suitable for production use, and\nshould only be enabled in development as needed.\n\nThe logs include:\n\n- All of Netty's low level networking, system configuration, memory leak\n  detection logs and more.\n- Hexadecimal representation of each batch of packets being transmitted to a\n  server or from a client.\n- Request routing, which is useful to debug a route that is not being matched.\n- Donkey trace logs.\n\n### Logging\n\nThe library doesn't include any logging implementation, and can be used with any\n[SLF4J](http://www.slf4j.org/) compatible logging library. The exception is when\nrunning in `debug` mode. In order to dynamically change the logging level\nwithout forcing users to add XML configuration files, Donkey\nuses [Logback](http://logback.qos.ch/) as its implementation. It should be\nincluded on the project's classpath, otherwise a warning will be printed and\ndebug logging will be disabled.\n\n## Troubleshooting\n\n#### ClassNotFoundException - com.codahale.metrics.JmxAttributeGauge\n\n```\nExecution error (ClassNotFoundException) at jdk.internal.loader.BuiltinClassLoader/loadClass (BuiltinClassLoader.java:581). com.codahale.metrics.JmxAttributeGauge\n```\n\nDonkey has a transitive dependency `io.dropwizard.metrics/metrics-core` version\n4.X.X. If you are using a library that is dependent on version 3.X.X then you\ncould get a dependency collision. To avoid it you can exclude the dependency\nwhen importing Donkey. For example:\n\nproject.clj\n\n```clojure\n:dependencies [com.appsflyer/donkey \"0.5.2\" :exclusions [io.dropwizard.metrics/metrics-core]]\n```   \n\ndeps.edn\n\n```clojure\n{:deps\n {com.appsflyer/donkey {:mvn/version \"0.5.2\"\n                       :exclusions [io.dropwizard.metrics/metrics-core]}}}\n```\n\n## License\n\nCopyright 2020 AppsFlyer\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use\nthis file except in compliance with the License. You may obtain a copy of the\nLicense at http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied. See the License for the\nspecific language governing permissions and limitations under the License.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappsflyer%2Fdonkey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fappsflyer%2Fdonkey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappsflyer%2Fdonkey/lists"}