{"id":19293947,"url":"https://github.com/igrishaev/with-http","last_synced_at":"2026-02-17T16:00:49.075Z","repository":{"id":195780001,"uuid":"693708320","full_name":"igrishaev/with-http","owner":"igrishaev","description":null,"archived":false,"fork":false,"pushed_at":"2023-09-26T08:30:06.000Z","size":43,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-11-27T18:36:13.873Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2023-09-19T14:52:34.000Z","updated_at":"2023-09-25T13:41:10.000Z","dependencies_parsed_at":"2023-09-19T15:29:20.035Z","dependency_job_id":"c013322c-54c2-4789-9de8-04eb8ad3bf7d","html_url":"https://github.com/igrishaev/with-http","commit_stats":null,"previous_names":["igrishaev/with-http"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/igrishaev/with-http","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fwith-http","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fwith-http/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fwith-http/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fwith-http/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/with-http/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fwith-http/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29549203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T14:33:00.708Z","status":"ssl_error","status_checked_at":"2026-02-17T14:32:58.657Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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-11-09T22:36:39.466Z","updated_at":"2026-02-17T16:00:49.057Z","avatar_url":"https://github.com/igrishaev.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# with-http\n\nA powerful macro to stub HTTP calls with a local Jetty server. Declarative,\nflexible, and extremely useful. I've been copying it through many projects, and\nnow it's time to ship it as a standalone library.\n\n**ToC**\n\n\u003c!-- toc --\u003e\n\n- [Installation](#installation)\n- [About](#about)\n- [The App routes](#the-app-routes)\n- [Default handler](#default-handler)\n- [Basic test](#basic-test)\n- [JSON](#json)\n- [Slow responses](#slow-responses)\n- [Files \u0026 Resources](#files--resources)\n- [Capturing requests](#capturing-requests)\n- [License](#license)\n\n\u003c!-- tocstop --\u003e\n\n## Installation\n\nLein:\n\n```clojure\n[com.github.igrishaev/with-http \"0.1.1\"]\n```\n\nDeps.edn\n\n```clojure\n{com.github.igrishaev/with-http {:mvn/version \"0.1.1\"}}\n```\n\n*Pay attention*: since the library is primarily used for tests, put the\ndependency in the corresponding profile or alias. Storing it in global\ndependencies is not a good idea as it becomes a part of the production code\notherwise.\n\n## About\n\nThe library provides a `with-http` macro of the following form:\n\n~~~clojure\n(with-http [port app]\n  ...body)\n~~~\n\nThe `port` is the number (1..65535) and the `app` is a map of routes. When\nentering the macro, it spawns a local Jetty server on that port in the\nbackground. The `app` map tells the server how to respond to calls.\n\n[aero]: https://github.com/juxt/aero\n\nNow that you have a running server, point your HTTP API clients to\n`http://localhost:\u003cport\u003e` to imitate real network interaction. For example, for\nprod, a third-party base URL is https://api.some.cool.service but for tests, it\nis http://localhost:8088. This can be done using environment variables or the\n[Aero library][aero].\n\nWhy not use `with-redefs`, would you ask? Well, although `with-redefs` looks like a solution at first glance, it's questionable. Using `with-redefs` means lying to yourself. You temporarily\nmute some pieces of the codebase pretending it's OK, but it's not.\n\nOften, bugs lurk in the code that you actually substitute using `with-redefs`,\nnamely:\n\n- you've messed up with MD5/SHA/etc algorithms to sign a request. Calling\n  localhost would trigger that code and lead to an exception, but `with-redefs`\n  would not.\n\n- you process the response poorly, e.g. not taking Content-Type header or\n  non-200 status code into account.\n\n- you cannot imitate delays and timeout exceptions when interacting with HTTP\n  API.\n\nThe good news is, that the `with-http` macro can test all the cases mentioned above and much more.\n\n## The App routes\n\nThe `app` parameter is a two-level map of the form:\n\n~~~clojure\n{path {method response}}\n~~~\n\nFor example:\n\n~~~clojure\n{\"/foo\" {:get {:status 200 :body \"it was GET\"}\n         :post {:status 201 :body \"it was POST\"}}}\n~~~\n\nCalling `GET /foo` and `POST /foo` would return 200 and 201 status codes with\ndifferent messages.\n\nThe response might be:\n\n- a Ring map;\n- a Ring handler function that accepts a request map and returns a response map;\n- an instance of `java.io.File`;\n- a resource: `(clojure.java.io/resource \"some/file.txt\")`;\n- a string.\n\nOther examples:\n\n~~~clojure\n{\"/foo\" {:get (fn [{:keys [params]}]\n                (log/infof \"Params: %s\" params)\n                {:status 200 :body \"OK\"})}}\n\n{\"/some/json\" {:get (io/resource \"file.json\")}}\n~~~\n\nThe path might be a vector as well. During the preparation step, it will be\ncompiled into a string, for example:\n\n~~~clojure\n{[\"/foo/bar/\" 42 \"/test\"]\n {:get {:status 200 :body \"hello\"}}}\n\n;; becomes\n\n{\"/foo/bar/42/test\"\n {:get {:status 200 :body \"hello\"}}}\n~~~\n\nThis is useful when the paths contain parameters.\n\nThe `make-url` function helps to build a local URL like\n`http://localhost:\u003cport\u003e/\u003cpath\u003e`. Its second `path` argument is either a string\nor a vector which gets compiled into a string:\n\n~~~clojure\n(make-url PORT \"/foo?a=1\u0026b=2\")\n;; http://localhost:8899/foo?a=1\u0026b=2\n\n(make-url 8899 [\"/users/\" 42 \"/reports/\" 99999])\n;; http://localhost:8899/users/42/reports/99999\n~~~\n\n## Default handler\n\nThe App mapping has a default handler which gets triggered when the client calls\na non-existing route. By default, it's 404 status page with the following JSON\npayload:\n\n~~~clojure\n(def NOT-FOUND\n  {:status 404\n   :body {:error \"with-http: route not found\"}})\n~~~\n\nYou can override it by adding the `:default` key to the app map. The value might\nbe a map, a function, a file and so on.\n\n~~~clojure\n{\"/foo\" {:get {:status 200 :body \"hello\"}}\n :default {:status 202 :body \"I'm the default!\"}}\n~~~\n\n## Basic test\n\nA simple test to ensure the macro works:\n\n~~~clojure\n(deftest test-with-http-test-json\n\n  (let [app\n        {\"/foo\" {:get {:status 200\n                       :body \"test\"}}}\n\n        url\n        (make-url PORT \"/foo\")\n\n        {:keys [status body]}\n        (with-http [PORT app]\n          (client/get url))]\n\n    (is (= 200 status))\n    (is (= \"test\" body))))\n~~~\n\n## JSON\n\nThe Ring handler function produced from the `app` mapping is wrapped with\n`wrap-json-response` and `wrap-json-params` middleware layers. It means the body\nof the response might be a collection that gets dumped into JSON:\n\n~~~clojure\n(deftest test-with-http-test-json\n\n  (let [body\n        {:hello [1 \"test\" true]}\n\n        app\n        {\"/foo\" {:get {:status 200\n                       :body body}}}\n\n        url\n        (make-url PORT \"/foo\")\n\n        {:keys [status body]}\n        (with-http [PORT app]\n          (client/get url {:as :json}))]\n\n    (is (= 200 status))\n    (is (= {:hello [1 \"test\" true]} body))))\n~~~\n\n## Slow responses\n\nTo imitate slow responses, provide a function that sleeps for a certain amount\nof time:\n\n~~~clojure\n{\"/foo\" {:get (fn [_]\n                (Thread/sleep 10000)\n                {:status 200 :body \"OK\"})}}\n~~~\n\nThen ensure you pass the timeout limit into your API call.\n\n## Files \u0026 Resources\n\nStoring JSON responses in files is a good idea. Here is how you can serve them\nwith the macro:\n\n~~~clojure\n{\"/foo\" {:get (io/file \"dev-resources/test.txt\")}}\n~~~\n\nor\n\n~~~clojure\n{\"/foo\" {:get (io/file \"dev-resources/test.json\")}}\n~~~\n\n## Capturing requests\n\nAnother trick to improve your tests: ensure you pass the right parameters or\nheaders to the HTTP API. Provide an atom and a handler function closed over that\natom. Each time you receive a request, save it's data to the atom and then\nvalidate them:\n\n~~~clojure\n(deftest test-with-http-capture-params\n\n  (let [capture!\n        (atom nil)\n\n        app\n        {\"/foo\" {:get (fn [{:keys [params]}]\n                        (reset! capture! params)\n                        {:status 200 :body \"OK\"})}}\n\n        url\n        (make-url PORT \"/foo?a=1\u0026b=2\")\n\n        {:keys [status body]}\n        (with-http [PORT app]\n          (client/get url))]\n\n    (is (= 200 status))\n    (is (= \"OK\" body))\n    (is (= {:a \"1\" :b \"2\"} @capture!))))\n~~~\n\n## License\n\nCopyright © 2023 Ivan Grishaev\n\nThis program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0.\n\nThis Source Code may also be made available under the following Secondary\nLicenses when the conditions for such availability set forth in the Eclipse\nPublic License, v. 2.0 are satisfied: GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or (at your\noption) any later version, with the GNU Classpath Exception which is available\nat https://www.gnu.org/software/classpath/license.html.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fwith-http","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Fwith-http","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fwith-http/lists"}