pax_global_header00006660000000000000000000000064140707523100014511gustar00rootroot0000000000000052 comment=44ce155754dbb375837dd1729b97e55c96046f9d clj-http-3.12.3/000077500000000000000000000000001407075231000133245ustar00rootroot00000000000000clj-http-3.12.3/.github/000077500000000000000000000000001407075231000146645ustar00rootroot00000000000000clj-http-3.12.3/.github/workflows/000077500000000000000000000000001407075231000167215ustar00rootroot00000000000000clj-http-3.12.3/.github/workflows/clojure.yml000066400000000000000000000017031407075231000211100ustar00rootroot00000000000000name: Clojure CI on: push: branches: [ 3.x ] pull_request: branches: [ 3.x ] jobs: build: runs-on: ubuntu-latest strategy: matrix: java: ["8", "11", "14"] clojure: ["1.6", "1.7", "1.8", "1.9", "1.10"] name: Java ${{ matrix.java }} Clojure ${{ matrix.clojure }} steps: - uses: actions/checkout@v2 - uses: actions/cache@v2 with: path: ~/.m2/repository key: ${{ runner.os }}-lein-${{ hashFiles('**/project.clj') }} restore-keys: | ${{ runner.os }}-lein- - name: Setup java uses: actions/setup-java@v1 with: java-version: ${{ matrix.java }} - name: Install dependencies run: lein deps - name: Run tests run: lein with-profile dev,${{matrix.clojure}} test :all - name: Check Reflection Warnings run: '! lein with-profile dev,${{matrix.clojure}} check 2>&1 | egrep "Reflection warning|Performance warning"' clj-http-3.12.3/.gitignore000066400000000000000000000005551407075231000153210ustar00rootroot00000000000000# leiningen .gitignore defaults /target /classes /checkouts pom.xml pom.xml.asc *.jar *.class /.lein-* /.nrepl-port # custom from here on out build lib *.dot # use glob syntax. syntax: glob creds.clj Manifest.txt aws.clj *.ser *~ *.bak *.off *.old .DS_Store *.#* *#* *.classpath *.project *.settings *.pyc docs/* doc http.log # Intellij Idea /*.iml /.idea log/ clj-http-3.12.3/CONTRIBUTING.md000066400000000000000000000012221407075231000155520ustar00rootroot00000000000000# Contributing Guidelines First, thanks for the contributing! Hopefully you find it fairly painless, but in the interest of explanation, here are some things you might be interested in when contributing code: - Please run the tests locally if you submit a change, you can use `lein all test :all` to ensure that they pass locally - If you're able, adding tests with a PR is fantastic! If not, no worries, I can add those later - Don't hesitate to ask if you have questions, use `@dakrone` or you can email me (if it's something you can't talk about publically) at `lee [at] writequit.org` That's it, thanks for using and contributing to clj-http! clj-http-3.12.3/LICENSE000066400000000000000000000020701407075231000143300ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2013 M. Lee Hinman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. clj-http-3.12.3/README.org000066400000000000000000001765541407075231000150140ustar00rootroot00000000000000#+TITLE: clj-http documentation #+AUTHOR: Lee Hinman #+STARTUP: align fold nodlcheck lognotestate showall #+OPTIONS: H:4 num:nil toc:t \n:nil @:t ::t |:t ^:{} -:t f:t *:t #+OPTIONS: skip:nil d:(HIDE) tags:not-in-toc auto-id:t #+PROPERTY: header-args :results code :exports both :noweb yes #+HTML_HEAD: #+LANGUAGE: en [[https://clojars.org/clj-http][file:https://img.shields.io/clojars/v/clj-http.svg]] [[https://github.com/dakrone/clj-http/actions?query=workflow%3A%22Clojure+CI%22][file:https://github.com/dakrone/clj-http/workflows/Clojure%20CI/badge.svg]] [[https://gitter.im/clj-http/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge][file:https://badges.gitter.im/clj-http/Lobby.svg]] * Table of Contents :TOC_3: :PROPERTIES: :CUSTOM_ID: h-aaf075ea-2f0e-4a45-871a-0f89c838fb4b :END: - [[#branches][Branches]] - [[#introduction][Introduction]] - [[#overview][Overview]] - [[#philosophy][Philosophy]] - [[#installation][Installation]] - [[#quickstart][Quickstart]] - [[#head][HEAD]] - [[#get][GET]] - [[#put][PUT]] - [[#post][POST]] - [[#delete][DELETE]] - [[#async-http-request][Async HTTP Request]] - [[#cancelling-requests][Cancelling Requests]] - [[#coercions][Coercions]] - [[#input-coercion][Input coercion]] - [[#output-coercion][Output coercion]] - [[#headers][Headers]] - [[#query-string-parameters][Query-string parameters]] - [[#meta-tag-headers][Meta Tag Headers]] - [[#link-headers][Link Headers]] - [[#redirects][Redirects]] - [[#how-to-create-a-custom-redirectstrategy][How to create a custom RedirectStrategy]] - [[#cookies][Cookies]] - [[#cookiestores][Cookiestores]] - [[#keystores-trust-stores][Keystores, Trust-stores]] - [[#exceptions][Exceptions]] - [[#decompression][Decompression]] - [[#debugging][Debugging]] - [[#logging][Logging]] - [[#caching][Caching]] - [[#authentication][Authentication]] - [[#basic-auth][Basic Auth]] - [[#digest-auth][Digest Auth]] - [[#ntlm-auth][NTLM Auth]] - [[#oauth2][oAuth2]] - [[#advanced-usage][Advanced Usage]] - [[#raw-request][Raw Request]] - [[#boolean-options][Boolean options]] - [[#persistent-connections][Persistent Connections]] - [[#re-using-httpclient-between-requests][Re-using =HttpClient= between requests]] - [[#proxies][Proxies]] - [[#custom-middleware][Custom Middleware]] - [[#modifying-apache-specific-features-of-the-httpclientbuilder-and-httpasyncclientbuilder][Modifying Apache-specific features of the =HttpClientBuilder= and =HttpAsyncClientBuilder=]] - [[#incrementally-json-parsing][Incrementally JSON Parsing]] - [[#dns-resolution][DNS Resolution]] - [[#development][Development]] - [[#faking-responses][Faking Responses]] - [[#optional-dependencies][Optional Dependencies]] - [[#clj-http-lite][clj-http-lite]] - [[#troubleshooting][Troubleshooting]] - [[#verifyerror-class-orgcodehausjacksonsmilesmileparser-overrides-final-method-getbinaryvalue][VerifyError class org.codehaus.jackson.smile.SmileParser overrides final method getBinaryValue...]] - [[#nohttpresponseexception--due-to-stale-connections][NoHttpResponseException ... due to stale connections**]] - [[#tests][Tests]] - [[#testimonials][Testimonials]] - [[#other-libraries-providing-middleware][Other Libraries Providing Middleware]] - [[#license][License]] * Branches :PROPERTIES: :CUSTOM_ID: h-e390585c-cbd8-4e94-b36b-4e9c27c16720 :END: There are branches for the major version numbers: - 2.x (no longer maintained except for security issues) - 3.x (current stable releases and the main Github branch) - master (which is 4.x, unreleased, based on version 5 of the apache http client) * Introduction :PROPERTIES: :CUSTOM_ID: h-d893078a-b20b-4086-9272-3d9c28c86846 :END: ** Overview :PROPERTIES: :CUSTOM_ID: h-d8b17d06-124e-44fd-9c86-0399f39b0254 :END: clj-http is an HTTP library wrapping the [[http://hc.apache.org/][Apache HttpComponents]] client. This library has taken over from mmcgrana's clj-http. ** Philosophy :PROPERTIES: :CUSTOM_ID: h-aa21d07d-333b-4ff2-93a9-ffdca31d8949 :END: The design of =clj-http= is inspired by the [[https://github.com/ring-clojure/ring][Ring]] protocol for Clojure HTTP server applications. The client in =clj-http.core= makes HTTP requests according to a given Ring request map and returns [[https://github.com/ring-clojure/ring/blob/master/SPEC][Ring response maps]] corresponding to the resulting HTTP response. The function =clj-http.client/request= uses Ring-style middleware to layer functionality over the core HTTP request/response implementation. Methods like =clj-http.client/get= are sugar over this =clj-http.client/request= function. * Installation :PROPERTIES: :CUSTOM_ID: h-ddfce0e2-6797-4774-add5-d5cf5bfaaa17 :END: =clj-http= is available as a Maven artifact from [[http://clojars.org/clj-http][Clojars]]. With Leiningen/Boot: #+BEGIN_SRC clojure [clj-http "3.12.3"] #+END_SRC If you need an older version, a 2.x release is also available. #+BEGIN_SRC clojure [clj-http "2.3.0"] #+END_SRC clj-http 3.x supports clojure 1.6.0 and higher. clj-http 4.x will support clojure 1.7.0 and higher. * Quickstart :PROPERTIES: :CUSTOM_ID: h-65f0132e-1f96-4711-a84e-973817f37dd3 :END: The main HTTP client functionality is provided by the =clj-http.client= namespace. First, require it in the REPL: #+BEGIN_SRC clojure (require '[clj-http.client :as client]) #+END_SRC Or in your application: #+BEGIN_SRC clojure (ns my-app.core (:require [clj-http.client :as client])) #+END_SRC The client supports simple =get=, =head=, =put=, =post=, =delete=, =copy=, =move=, =patch=, and =options= requests. Response are returned as [[https://github.com/ring-clojure/ring/blob/master/SPEC][Ring-style response maps]]: ** HEAD :PROPERTIES: :CUSTOM_ID: h-79d1bb5f-c695-46a6-af4e-a64ca599c978 :END: #+BEGIN_SRC clojure (client/head "http://example.com/resource") (client/head "http://example.com/resource" {:accept :json}) #+END_SRC ** GET :PROPERTIES: :CUSTOM_ID: h-89c164fb-85c2-4953-a8c4-a50867adf42a :END: Example requests: #+BEGIN_SRC clojure (client/get "http://example.com/resources/id") ;; Setting options (client/get "http://example.com/resources/3" {:accept :json}) (client/get "http://example.com/resources/3" {:accept :json :query-params {"q" "foo, bar"}}) ;; Specifying headers as either a string or collection: (client/get "http://example.com" {:headers {"foo" ["bar" "baz"], "eggplant" "quux"}}) ;; Using either string or keyword header names: (client/get "http://example.com" {:headers {:foo ["bar" "baz"], :eggplant "quux"}}) ;; Completely ignore cookies: (client/post "http://example.com" {:cookie-policy :none}) ;; There are also multiple ways to handle cookies (client/post "http://example.com" {:cookie-policy :default}) (client/post "http://example.com" {:cookie-policy :netscape}) (client/post "http://example.com" {:cookie-policy :standard}) (client/post "http://example.com" {:cookie-policy :standard-strict}) ;; Cookies can be completely configurable with a custom spec by adding a ;; function to return a cookie spec for parsing the cookie. For example, if you ;; wanted to configure a spec provider to have a certain compatibility level: (client/post "http://example.com" {:cookie-spec (fn [http-context] (println "generating a new cookie spec") (.create (org.apache.http.impl.cookie.RFC6265CookieSpecProvider. org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/IE_MEDIUM_SECURITY (PublicSuffixMatcherLoader/getDefault)) http-context))}) ;; Or a version with relaxed compatibility (client/post "http://example.com" {:cookie-spec (fn [http-context] (println "generating a new cookie spec") (.create (org.apache.http.impl.cookie.RFC6265CookieSpecProvider. org.apache.http.impl.cookie.RFC6265CookieSpecProvider$CompatibilityLevel/RELAXED (PublicSuffixMatcherLoader/getDefault)) http-context))}) ;; Sometimes you want to do your own validation or something, which you can do ;; by proxying the CookieSpecBase. Note that this doesn't actually return the ;; cookies, because clj-http does its own cookie parsing. If you want to store ;; the cookies from these methods you'll need to use a cookie store or put it in ;; some datastructure yourself. (client/post "http://example.com" {:cookie-spec (fn [http-context] (proxy [org.apache.http.impl.cookie.CookieSpecBase] [] ;; Version and version header (getVersion [] 0) (getVersionHeader [] nil) ;; parse headers into cookie objects (parse [header cookie-origin] (java.util.ArrayList.)) ;; Validate a cookie, throwing MalformedCookieException if the ;; cookies isn't valid (validate [cookie cookie-origin] (println "validating:" cookie)) ;; Determine if a cookie matches the target location (match [cookie cookie-origin] true) ;; Format a list of cookies into a list of headers (formatCookies [cookies] (java.util.ArrayList.))))}) ;; If you have created your own registry for cookie policies, you can provide ;; :cookie-policy-registry to use it. See ;; clj-http.core/create-custom-cookie-policy-registry for an example of a custom ;; registry (client/post "http://example.com" {:cookie-policy-registry my-custom-policy-registry :cookie-policy "my-policy"}) ;; Need to contact a server with an untrusted SSL cert? (client/get "https://alioth.debian.org" {:insecure? true}) ;; If you don't want to follow-redirects automatically: (client/get "http://example.com/redirects-somewhere" {:redirect-strategy :none}) ;; Only follow a certain number of redirects: (client/get "http://example.com/redirects-somewhere" {:max-redirects 5}) ;; Avoid throwing exceptions if redirected too many times: (client/get "http://example.com/redirects-somewhere" {:max-redirects 5 :redirect-strategy :graceful}) ;; Throw an exception if the get takes too long. Timeouts in milliseconds. (client/get "http://example.com/redirects-somewhere" {:socket-timeout 1000 :connection-timeout 1000}) ;; Query parameters (client/get "http://example.com/search" {:query-params {"q" "foo, bar"}}) ;; "Nested" query parameters ;; (this yields a query string of `a[e][f]=6&a[b][c]=5`) (client/get "http://example.com/search" {:query-params {:a {:b {:c 5} :e {:f 6}}}}) ;; Provide cookies — uses same schema as :cookies returned in responses ;; (see the cookie store option for easy cross-request maintenance of cookies) (client/get "http://example.com" {:cookies {"ring-session" {:discard true, :path "/", :value "", :version 0}}}) ;; Tell clj-http not to decode cookies from the response header (client/get "http://example.com" {:decode-cookies false}) ;; Support for IPv6! (client/get "http://[2001:62f5:9006:e472:cabd:c8ff:fee3:8ddf]") ;; Super advanced, your own http-client-context and request-config (client/get "http://example.com/get" {:http-client-context my-http-client-context :http-request-config my-request-config}) #+END_SRC The client will also follow redirects on the appropriate =30*= status codes. The client transparently accepts and decompresses the =gzip= and =deflate= content encodings. =:trace-redirects= will contain the chain of the redirections followed. ** PUT :PROPERTIES: :CUSTOM_ID: h-1582cd6e-a6e8-49c8-96e3-28eee6128c31 :END: #+BEGIN_SRC clojure (client/put "http://example.com/api" {:body "my PUT body"}) #+END_SRC ** POST :PROPERTIES: :CUSTOM_ID: h-32c8ca7a-0ef2-41b8-8158-20b0e2945e5d :END: #+BEGIN_SRC clojure ;; Various options: (client/post "http://example.com/api" {:basic-auth ["user" "pass"] :body "{\"json\": \"input\"}" :headers {"X-Api-Version" "2"} :content-type :json :socket-timeout 1000 ;; in milliseconds :connection-timeout 1000 ;; in milliseconds :accept :json}) ;; Send form params as a urlencoded body (POST or PUT) (client/post "http://example.com" {:form-params {:foo "bar"}}) ;; Send form params as a json encoded body (POST or PUT) (client/post "http://example.com" {:form-params {:foo "bar"} :content-type :json}) ;; Send form params as a json encoded body (POST or PUT) with options (client/post "http://example.com" {:form-params {:foo "bar"} :content-type :json :json-opts {:date-format "yyyy-MM-dd"}}) ;; You can also specify the encoding of form parameters (client/post "http://example.com" {:form-params {:foo "bar"} :form-param-encoding "ISO-8859-1"}) ;; Send form params as a Transit encoded JSON body (POST or PUT) with options (client/post "http://example.com" {:form-params {:foo "bar"} :content-type :transit+json :transit-opts {:encode {:handlers {}} :decode {:handlers {}}}}) ;; Send form params as a Transit encoded MessagePack body (POST or PUT) with options (client/post "http://example.com" {:form-params {:foo "bar"} :content-type :transit+msgpack :transit-opts {:encode {:handlers {}} :decode {:handlers {}}}}) ;; Multipart form uploads/posts ;; takes a vector of maps, to preserve the order of entities, :name ;; will be used as the part name unless :part-name is specified (client/post "http://example.org" {:multipart [{:name "title" :content "My Awesome Picture"} {:name "Content/type" :content "image/jpeg"} {:name "foo.txt" :part-name "eggplant" :content "Eggplants"} {:name "file" :content (clojure.java.io/file "pic.jpg")}] ;; You can also optionally pass a :mime-subtype :mime-subtype "foo"}) ;; Multipart :content values can be one of the following: ;; String, InputStream, File, a byte-array, or an instance of org.apache.http.entity.mime.content.ContentBody ;; Some Multipart bodies can also support more keys (like :encoding ;; and :mime-type), check src/clj-http/multipart.clj to see all flags ;; Apache's http client automatically retries on IOExceptions, if you ;; would like to handle these retries yourself, you can specify a ;; :retry-handler. Return true to retry, false to stop trying: (client/post "http://example.org" {:multipart [["title" "Foo"] ["Content/type" "text/plain"] ["file" (clojure.java.io/file "/tmp/missing-file")]] :retry-handler (fn [ex try-count http-context] (println "Got:" ex) (if (> try-count 4) false true))}) ;; to handle a file with non-ascii filename, try :multipart-charset "UTF-8" and :multipart-mode BROWSER_COMPATIBLE ;; see also: https://stackoverflow.com/questions/3393445/international-characters-in-filename-in-mutipart-formdata (import (org.apache.http.entity.mime HttpMultipartMode)) (client/post "http://example.org" {:multipart [{:content (clojure.java.io/file "日本語.txt")}] :multipart-mode HttpMultipartMode/BROWSER_COMPATIBLE :multipart-charset "UTF-8"} ) #+END_SRC A word about flattening nested =:query-params= and =:form-params= maps. There are essentially three different ways to handle flattening them: - =:ignore-nested-query-string= :: Do not handle nested query parameters specially, treat them as the exact text they come in as. Defaults to *false*. - =:flatten-nested-form-params= :: Flatten nested (map within a map) =:form-params= before encoding it as the body. Defaults to *false*, meaning form params are encoded only =x-www-form-urlencoded=. - =:flatten-nested-keys= :: An advanced way of specifying which keys having nested maps should be flattened. A middleware function checks the previous two options (=:ignore-nested-query-string= and =:flatten-nested-form-params=) and modifies this to be the list that will be flattened. ** DELETE :PROPERTIES: :CUSTOM_ID: h-c7165d6b-232a-439d-9390-8c05e6ef1e6f :END: #+BEGIN_SRC clojure (client/delete "http://example.com/resource") #+END_SRC ** Async HTTP Request :PROPERTIES: :CUSTOM_ID: h-0e3eb987-5b2b-4874-97ef-b834394d083d :END: The new async HTTP request API is a Ring-style async API. All options for synchronous request can use in asynchronous requests. start an async request is easy, for example: #+BEGIN_SRC clojure ;; :async? in options map need to be true (client/get "http://example.com" {:async? true} ;; respond callback (fn [response] (println "response is:" response)) ;; raise callback (fn [exception] (println "exception message is: " (.getMessage exception)))) #+END_SRC All exceptions thrown during the request will be passed to the raise callback. *** Cancelling Requests :PROPERTIES: :CUSTOM_ID: cancelling-requests :END: Calls to the http methods with =:async true= return an Apache [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][BasicFuture]] that you can call =.get= or =.cancel= on. See the Javadocs for =BasicFuture= [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][here]]. For instance: #+BEGIN_SRC clojure (import '(java.util.concurrent TimeoutException TimeUnit)) (let [future (client/get "http://example.com/slow-url" {:async true :oncancel #(println "request was cancelled")} #(println :got %) #(println :err %))] (try (.get future 1 TimeUnit/SECONDS) (catch TimeoutException e ;; Cancel the request, it's taken too long (.cancel future true)))) #+END_SRC ** Coercions :PROPERTIES: :CUSTOM_ID: h-8902cd95-e01e-4d9b-9dc8-5f5c8f04504b :END: clj-http allows coercing the body of the request either before it is sent (input coercion), or after it's received (output coercion) from the server. *** Input coercion :PROPERTIES: :CUSTOM_ID: h-bed01743-2209-473d-ae86-bd187f059e0c :END: #+BEGIN_SRC clojure ;; body as a byte-array (client/post "http://example.com/resources" {:body my-byte-array}) ;; body as a string (client/post "http://example.com/resources" {:body "string"}) ;; :body-encoding is optional and defaults to "UTF-8" (client/post "http://example.com/resources" {:body "string" :body-encoding "UTF-8"}) ;; body as a file (client/post "http://example.com/resources" {:body (clojure.java.io/file "/tmp/foo") :body-encoding "UTF-8"}) ;; :length is optional for passing in an InputStream; if not ;; supplied it will default to -1 to signal to HttpClient to use ;; chunked encoding (client/post "http://example.com/resources" {:body (clojure.java.io/input-stream "/tmp/foo")}) (client/post "http://example.com/resources" {:body (clojure.java.io/input-stream "/tmp/foo") :length 1000}) #+END_SRC *** Output coercion :PROPERTIES: :CUSTOM_ID: h-0c8966a6-f220-4f1e-a79e-a520fb313f9e :END: #+BEGIN_SRC clojure ;; The default output is a string body (client/get "http://example.com/foo.txt") ;; Coerce as a byte-array (client/get "http://example.com/favicon.ico" {:as :byte-array}) ;; Coerce as something other than UTF-8 string (client/get "http://example.com/string.txt" {:as "UTF-16"}) ;; Coerce as json (client/get "http://example.com/foo.json" {:as :json}) (client/get "http://example.com/foo.json" {:as :json-string-keys}) ;; Coerce as Transit encoded JSON or MessagePack (client/get "http://example.com/foo" {:as :transit+json}) (client/get "http://example.com/foo" {:as :transit+msgpack}) ;; Coerce as a clojure datastructure (client/get "http://example.com/foo.clj" {:as :clojure}) ;; Coerce as x-www-form-urlencoded (client/post "http://example.com/foo" {:as :x-www-form-urlencoded}) ;; Try to automatically coerce the output based on the content-type ;; header (this is currently a BETA feature!). Currently supports ;; text, json and clojure (with automatic charset detection) ;; clojure coercion requires "application/clojure" or ;; "application/edn" in the content-type header (client/get "http://example.com/foo.json" {:as :auto}) ;; Return the body as a stream (client/get "http://example.com/bigrequest.html" {:as :stream}) ;; Note that the connection to the server will NOT be closed until the ;; stream has been read ;; Return the body as a java.io.BufferedReader (client/get "http://example.com/bigrequest.html" {:as :reader}) ;; As above, the connection will remain open until the stream has been ;; read. The reader will attempt to respect the server-specified charset, ;; if any, defaulting to UTF-8. #+END_SRC Output coercion with =:as :json=, =:as :json-string-keys= or =:as :x-www-form-urlencoded=, will only work with an optional dependency, see [[#optional-dependencies][Optional Dependencies]]. By default, JSON coercion is only applied when the response's status is considered "unexceptional". If the =:unexceptional-status= option is provided, then its value is a function which specifies what status codes are unexceptional. =:unexceptional-status= defaults to =clj-http.client/unexceptional-status?=. If you would like to change under what conditions coercion is applied, you can send the =:coerce= option, which can be set to: #+BEGIN_SRC clojure :always ;; always json decode the body :unexceptional ;; json decode when an HTTP response is considered unexceptional :exceptional ;; json decode when an HTTP response is considered exceptional #+END_SRC The =:coerce= setting defaults to =:unexceptional=. ** Headers :PROPERTIES: :CUSTOM_ID: h-ef64574f-f9dc-4356-95b7-d55cc6737b44 :END: clj-http's treatment of headers is a little more permissive than the [[https://github.com/ring-clojure/ring/blob/master/SPEC][ring spec]] specifies. Rather than forcing all request headers to be lowercase strings, clj-http allows strings or keywords of any case. Keywords will be transformed into their canonical representation, so the :content-md5 header will be sent to the server as "Content-MD5", for instance. String keys in request headers, however, will be sent to the server with their casing unchanged. Response headers can be read as keywords or strings of any case. If the server responds with a "Date" header, you could access the value of that header as :date, "date", "Date", etc. If for some reason you require access to the original header name that the server specified, it is available by invoking (keys ...) on the header map. This special treatment of headers is implemented in the wrap-header-map middleware, which (like any middleware) can be disabled by using with-middleware to specify different behavior. ** Query-string parameters :PROPERTIES: :CUSTOM_ID: h-dd49992c-a516-4af0-9735-4f4340773361 :END: There are four different ways that query string parameters for array values can be generated, depending on what the resulting query string should look like, they are: - A repeating parameter (default) - Array style - Indexed array style - Comma separated style Here is an example of the input and output for the ~:query-params~ parameter, controlled by the ~:multi-param-style~ option: #+BEGIN_SRC clojure ;; default style, with :multi-param-style unset :a [1 2 3] => "a=1&a=2&a=3" ;; with :multi-param-style :array, a repeating param with array suffix ;; (PHP-style): :a [1 2 3] => "a[]=1&a[]=2&a[]=3" ;; with :multi-param-style :indexed, a repeating param with array suffix and ;; index (Rails-style): :a [1 2 3] => "a[0]=1&a[1]=2&a[2]=3" ;; with :multi-param-style :comma-separated, a param with comma-separated values :a [1 2 3] => "a=1,2,3" #+END_SRC ** Meta Tag Headers :PROPERTIES: :CUSTOM_ID: h-01663a63-8bc8-45da-8a3d-341402f3f3fa :END: HTML 4.01 allows using the tag ~~ and HTML 5 allows using the tag ~~ to specify a header that should be treated as an HTTP response header. By default, clj-http will ignore the body of the response (other than the regular output coercion), but if you need clj-http to parse the headers out of the body, you can use the =:decode-body-headers= option: #+BEGIN_SRC clojure ;; without decoding body headers (defaults to off): (:headers (client/get "http://www.yomiuri.co.jp/")) => {"server" "Apache", "content-encoding" "gzip", "content-type" "text/html", "date" "Tue, 09 Oct 2012 18:02:41 GMT", "cache-control" "max-age=0, no-cache", "expires" "Tue, 09 Oct 2012 18:02:41 GMT", "etag" "\"1dfb-2686-4cba2686fb8b1\"", "pragma" "no-cache", "connection" "close"} ;; with decoding body headers, notice the content-type, ;; content-style-type and content-script-type headers: (:headers (client/get "http://www.yomiuri.co.jp/" {:decode-body-headers true})) => {"server" "Apache", "content-encoding" "gzip", "content-script-type" "text/javascript", "content-style-type" "text/css", "content-type" "text/html; charset=Shift_JIS", "date" "Tue, 09 Oct 2012 18:02:59 GMT", "cache-control" "max-age=0, no-cache", "expires" "Tue, 09 Oct 2012 18:02:59 GMT", "etag" "\"1dfb-2686-4cba2686fb8b1\"", "pragma" "no-cache", "connection" "close"} #+END_SRC This can be used to have clj-http correctly interpret the body's charset by using: #+BEGIN_SRC clojure (client/get "http://www.yomiuri.co.jp/" {:decode-body-headers true :as :auto}) => ;; correctly formatted :body (Shift_JIS charset instead of UTF-8) #+END_SRC Note that this feature is currently beta and uses [[https://github.com/weavejester/crouton][Crouton]] to parse the body of the request. If you do not want to use this feature, you can include Crouton in addition to clj-http as a dependency like so: #+BEGIN_SRC clojure (defproject foo "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.3.0"] [clj-http "0.6.0"] [crouton "1.0.0"]]) #+END_SRC Note also that HEAD requests will not return a body, in which case this setting will have no effect. clj-http will automatically disable the =:decode-body-headers= option. ** Link Headers :PROPERTIES: :CUSTOM_ID: h-f7464c54-4928-474f-9132-08e6b6f3c19d :END: clj-http parses any [[http://tools.ietf.org/html/rfc5988][link headers]] returned in the response, and adds them to the =:links= key on the response map. This is particularly useful for paging RESTful APIs: #+BEGIN_SRC clojure (:links (client/get "https://api.github.com/gists")) => {:next {:href "https://api.github.com/gists?page=2"} :last {:href "https://api.github.com/gists?page=22884"}} #+END_SRC ** Redirects :PROPERTIES: :CUSTOM_ID: h-71c966ae-f764-4bd7-801c-0f3c8413c502 :END: clj-http conforms its behaviour regarding automatic redirects to the [[https://tools.ietf.org/html/rfc2616#section-10.3][RFC]]. It means that redirects on status =301=, =302=, =307= and =308= are not redirected on methods other than =GET= and =HEAD=. If you want a behaviour closer to what most browser have, you can set =:redirect-strategy= to =:lax= in your request to have automatic redirection work on all methods by transforming the method of the request to =GET=. Redirect Options: - =:trace-redirects= :: If true, clj-http will enhance the response object with a list of redirected URLs with key: =:trace-redirects=. - =:redirect-strategy= :: Sets the redirect strategy for clj-http. Accepts the following: - =:none= - Perform no redirects - =:default= - See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/DefaultRedirectStrategy.html - =:lax= - See https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/LaxRedirectStrategy.html - =:graceful= - Similar to =:default=, but does not throw exceptions when max redirects is reached. This is the redirects behaviour in 2.x - =nil= - When nil, assumes =:default= You may also pass in an instance of RedirectStrategy (in the =:redirect-strategy= key) if you want a behavior that's not implemented. Additionally, clj-http will attempt to validate that a redirect host is not invalid, you can disable this by setting =:validate-redirects false= in the request (the default is true) NOTE: The options =:force-redirects= and =:follow-redirects= (present in clj-http 2.x are no longer used). You can use =:graceful= to mostly emulate the old redirect behaviour. *** How to create a custom RedirectStrategy :PROPERTIES: :CUSTOM_ID: h:a3b8b124-411f-4c4c-ac4b-777624e76bf1 :END: As mentioned earlier, it's possible to pass a custom instance of RedirectStrategy. The snippet below shows how to create a custom =RedirectStrategy= by wrapping the default strategy. #+begin_src clojure (def default-strategy org.apache.http.impl.client.DefaultRedirectStrategy/INSTANCE) (def logging-redirect-strategy (reify org.apache.http.client.RedirectStrategy (getRedirect [this request response context] (println "attempting redirect...") (.getRedirect default-strategy request response context)) (isRedirected [this request response context] (println "checking isRedirected") (.isRedirected default-strategy request response context)))) (client/get "https://httpbin.org/absolute-redirect/3" {:redirect-strategy logging-redirect-strategy}) ;; this will output the following: ;; ;; checking isRedirected ;; attempting redirect... ;; checking isRedirected ;; attempting redirect... ;; checking isRedirected ;; attempting redirect... ;; checking isRedirected #+end_src ** Cookies :PROPERTIES: :CUSTOM_ID: h-3bb89b16-4be3-455e-98ec-c5ca5830ddb9 :END: *** Cookiestores :PROPERTIES: :CUSTOM_ID: h-1d86fe30-f690-4c2a-9a1c-231669f4591a :END: clj-http can simplify the maintenance of cookies across requests if it is provided with a _cookie store_. #+BEGIN_SRC clojure (binding [clj-http.core/*cookie-store* (clj-http.cookies/cookie-store)] (client/post "http://example.com/login" {:form-params {:username "..." :password "..."}}) (client/get "http://example.com/secured-page") ...) #+END_SRC (The =clj-http.cookies/cookie-store= function returns a new empty instance of a default implementation of =org.apache.http.client.CookieStore=.) This will allow cookies to only be _written_ to the cookie store. Cookies from the cookie-store will not automatically be sent with future requests. If you would like cookies from the cookie-store to automatically be sent with each request, specify the cookie-store with the =:cookie-store= option: #+BEGIN_SRC clojure (let [my-cs (clj-http.cookies/cookie-store)] (client/post "http://example.com/login" {:form-params {:username "..." :password "..."} :cookie-store my-cs}) (client/post "http://example.com/update" {:body my-data :cookie-store my-cs})) #+END_SRC You can also use the =get-cookies= function to retrieve the cookies from a cookie store: #+BEGIN_SRC clojure (def cs (clj-http.cookies/cookie-store)) (client/get "http://google.com" {:cookie-store cs}) (clojure.pprint/pprint (clj-http.cookies/get-cookies cs)) {"NID" {:domain ".google.com", :expires #, :path "/", :value "58=c387....", :version 0}, "PREF" {:domain ".google.com", :expires #, :path "/", :value "ID=3ba...:FF=0:TM=133...:LM=133...:S=_iRM...", :version 0}} #+END_SRC *** Keystores, Trust-stores :PROPERTIES: :CUSTOM_ID: h-7968467a-1441-4a73-9307-9a7a5fd8e733 :END: You can also specify your own keystore/trust-store to be used: #+BEGIN_SRC clojure (client/get "https://example.com" {:keystore "/path/to/keystore.ks" :keystore-type "jks" ; default: jks :keystore-pass "secretpass" :trust-store "/path/to/trust-store.ks" :trust-store-type "jks" ; default jks :trust-store-pass "trustpass"}) #+END_SRC The =:keystore/:trust-store= values may be either paths to keystore files or =KeyStore= instances. ** Exceptions :PROPERTIES: :CUSTOM_ID: h-ed9e04f1-1c7b-4c2e-9259-94d2a3e65a89 :END: The client will throw exceptions on, well, exceptional status codes, meaning all HTTP responses other than =#{200 201 202 203 204 205 206 207 300 301 302 303 304 307}=. clj-http will throw a [[http://github.com/scgilardi/slingshot][Slingshot]] Stone that can be caught by a regular =(catch Exception e ...)= or in Slingshot's =try+= block: #+BEGIN_SRC clojure (client/get "http://example.com/broken") => ExceptionInfo clj-http: status 404 clj-http.client/wrap-exceptions/fn--583 (client.clj:41) ;; Or, if you would like the Exception message to contain the entire response: (client/get "http://example.com/broken" {:throw-entire-message? true}) => ExceptionInfo clj-http: status 404 {:status 404, :headers {"server" "nginx/1.0.4", "x-runtime" "12ms", "content-encoding" "gzip", "content-type" "text/html; charset=utf-8", "date" "Mon, 17 Oct 2011 23:15 :36 GMT", "cache-control" "no-cache", "status" "404 Not Found", "transfer-encoding" "chunked", "connection" "close"}, :body "...body here..."} clj-http.client/wrap-exceptions/fn--584 (client.clj:42 ;; You can also ignore HTTP-status-code exceptions and handle them yourself: (client/get "http://example.com/broken" {:throw-exceptions false}) ;; Or ignore an unknown host (methods return 'nil' if this is set to ;; true and the host does not exist: (client/get "http://example.invalid" {:ignore-unknown-host? true}) ;; Or customize the http statuses that will not throw: (client/get "http://example.com/broken" {:unexceptional-status #(<= 200 % 299)}) #+END_SRC (spacing added by me to be human readable) How to use with Slingshot: #+BEGIN_SRC clojure ; Response map is thrown as exception obj. ; We filter out by status codes (try+ (client/get "http://example.com/broken") (catch [:status 403] {:keys [request-time headers body]} (log/warn "403" request-time headers)) (catch [:status 404] {:keys [request-time headers body]} (log/warn "NOT Found 404" request-time headers body)) (catch Object _ (log/error (:throwable &throw-context) "unexpected error") (throw+))) #+END_SRC ** Decompression :PROPERTIES: :CUSTOM_ID: h-f780c96c-90be-4d83-9b53-227a9e5942ab :END: By default, clj-http will add the ={"Accept-Encoding" "gzip, deflate"}= header to requests, and automatically decompress the resulting gzip or deflate stream if the =Content-Encoding= header is found on the response. If this is undesired, the ={:decompress-body false}= option can be specified: #+BEGIN_SRC clojure ;; Auto-decompression used: (google requires a user-agent to send gzip data) (def h {"User-Agent" "Mozilla/5.0 (Windows NT 6.1;) Gecko/20100101 Firefox/13.0.1"}) (def resp (client/get "http://google.com" {:headers h})) (:orig-content-encoding resp) => "gzip" ;; <= google sent response gzipped ;; and without decompression: (def resp2 (client/get "http://google.com" {:headers h :decompress-body false}) (:orig-content-encoding resp2) => nil #+END_SRC If clj-http decompresses something, the "content-encoding" header is removed from the headers (because the encoding is no longer true). This allows clj-http to be used as a pass-through proxy with ring. The original content-encoding is available as =:orig-content-encoding= in the response map if auto-decompression is enabled. ** Debugging :PROPERTIES: :CUSTOM_ID: debugging :END: There are four debugging methods you can use: #+BEGIN_SRC clojure ;; print request info to *out*: (client/get "http://example.org" {:debug true}) ;; print request info to *out*, including request body: (client/post "http://example.org" {:debug true :debug-body true :body "..."}) ;; save the request that was sent in a :request key in the response: (client/get "http://example.org" {:save-request? true}) ;; save the request that was sent in a :request key in the response, ;; including the body content: (client/get "http://example.org" {:save-request? true :debug-body true}) ;; add an HttpResponseInterceptor to the request. This callback ;; is called for each redirects with the following args: ;; ^HttpResponse resp, HttpContext^ ctx ;; this allows low level debugging + access to socket. ;; see http://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/HttpResponseInterceptor.html (client/get "http://example.org" {:response-interceptor (fn [resp ctx] (println ctx))}) #+END_SRC *** Logging :PROPERTIES: :CUSTOM_ID: h-0d505652-d453-48a2-a868-46aef2b8af66 :END: Finally, if you want to access the logging that the Apache client does internally, you can set up your dependencies to add the [[https://logging.apache.org/log4j/2.x/][log4j2]] libraries and configure the logging for clj-http. In order to do this, you'll need to add #+BEGIN_SRC clojure [org.apache.logging.log4j/log4j-api "2.11.0"] [org.apache.logging.log4j/log4j-core "2.11.0"] [org.apache.logging.log4j/log4j-1.2-api "2.11.0"] #+END_SRC To your =project.clj= and have a usable log4j2.properties. I have provided one in =resources/log4j2.properties=. Make sure to set: #+BEGIN_SRC fundamental rootLogger.level = debug #+END_SRC If you want to see debug information (or "trace" for trace logging). When you perform a request you should see something akin to this in the logs: #+BEGIN_SRC fundamental [2018-03-20T20:36:34,635][DEBUG][o.a.h.c.p.RequestAddCookies] CookieSpec selected: default [2018-03-20T20:36:34,635][DEBUG][o.a.h.c.p.RequestAuthCache] Auth cache not set in the context [2018-03-20T20:36:34,635][DEBUG][o.a.h.i.c.BasicHttpClientConnectionManager] Get connection for route {s}->https://example.com:443 [2018-03-20T20:36:34,636][DEBUG][o.a.h.i.c.DefaultManagedHttpClientConnection] http-outgoing-1: set socket timeout to 0 [2018-03-20T20:36:34,636][DEBUG][o.a.h.i.e.MainClientExec ] Opening connection {s}->https://example.com:443 [2018-03-20T20:36:34,644][DEBUG][o.a.h.i.c.DefaultHttpClientConnectionOperator] Connecting to example.com/10.0.0.1:443 [2018-03-20T20:36:34,644][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Connecting socket to example.com/10.0.0.1:443 with timeout 0 [2018-03-20T20:36:34,692][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Enabled protocols: [TLSv1, TLSv1.1, TLSv1.2] [2018-03-20T20:36:34,693][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Enabled cipher suites:[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, ... etc ...] [2018-03-20T20:36:34,693][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Starting handshake [2018-03-20T20:36:34,841][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] Secure session established [2018-03-20T20:36:34,842][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] negotiated protocol: TLSv1.2 [2018-03-20T20:36:34,842][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] negotiated cipher suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] peer principal: CN=example.com [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] peer alternative names: [example.com, www.example.com] [2018-03-20T20:36:34,843][DEBUG][o.a.h.c.s.SSLConnectionSocketFactory] issuer principal: CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.c.DefaultHttpClientConnectionOperator] Connection established 192.168.0.29:36792<->10.0.0.1:443 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Executing request POST /post HTTP/1.1 [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Target auth state: UNCHALLENGED [2018-03-20T20:36:34,844][DEBUG][o.a.h.i.e.MainClientExec ] Proxy auth state: UNCHALLENGED [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> POST /post HTTP/1.1 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Connection: close [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> accept-encoding: gzip, deflate [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Content-Length: 14 [2018-03-20T20:36:34,845][DEBUG][o.a.h.headers ] http-outgoing-1 >> Content-Type: text/plain; charset=UTF-8 [2018-03-20T20:36:34,846][DEBUG][o.a.h.headers ] http-outgoing-1 >> Host: example.com [2018-03-20T20:36:34,846][DEBUG][o.a.h.headers ] http-outgoing-1 >> User-Agent: Apache-HttpClient/4.5.5 (Java/9.0.1) [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "POST /post HTTP/1.1[\r][\n]" [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Connection: close[\r][\n]" [2018-03-20T20:36:34,846][DEBUG][o.a.h.wire ] http-outgoing-1 >> "accept-encoding: gzip, deflate[\r][\n]" [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Content-Length: 14[\r][\n]" [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Content-Type: text/plain; charset=UTF-8[\r][\n]" [2018-03-20T20:36:34,847][DEBUG][o.a.h.wire ] http-outgoing-1 >> "Host: example.com[\r][\n]" etc etc it will go on forever and be very verbose #+END_SRC This provides both the data sent and received on the wire for debugging purposes. I've also provided an example for changing the log level from clojure in =examples/logging-apache-requests.clj=. * Caching :PROPERTIES: :CUSTOM_ID: h-2c4ee611-ca22-432e-9c33-18040566661e :END: clj-http supports Apache's caching client, essentially it "provides an HTTP/1.1-compliant caching layer to be used with HttpClient--the Java equivalent of a browser cache." (see [[https://hc.apache.org/httpcomponents-client-ga/tutorial/html/caching.html][the explanation in the apache docs]]). In order to use the cache, a reusable connection manager *and* http-client must be used. An example of basic usage with the default options: #+BEGIN_SRC clojure (let [cm (conn/make-reusable-conn-manager {}) client (:http-client (http/get "http://example.com" {:connection-manager cm :cache true}))] (http/get "http://example.com" {:connection-manager cm :http-client client :cache true}) (http/get "http://example.com" {:connection-manager cm :http-client client :cache true}) (http/get "http://example.com" {:connection-manager cm :http-client client :cache true})) #+END_SRC You can build your own cache config by providing either a map of caching configuration options, or by providing a =CacheConfig= object, as seen below: #+BEGIN_SRC clojure (let [cm (conn/make-reusable-conn-manager {}) cache-config (core/build-cache-config {:cache-config {:max-object-size 4096}}) client (:http-client (http/get "http://example.com" {:connection-manager cm :cache true}))] (http/get "http://example.com" ;; Use the default cache config settings {:connection-manager cm :http-client client :cache true}) (http/get "http://example.com" {:connection-manager cm :http-client client :cache true ;; Provide cache configuration options as a map :cache-config {:max-object-size 9152 :max-cache-entries 100}}) (http/get "http://example.com" {:connection-manager cm :http-client client :cache true ;; Provide the cache configuration as a CacheConfig object :cache-config cache-config})) #+END_SRC In the response, clj-http provides the =:cached= key to indicate whether the response was cached, missed, etc: - nil :: Caching was not used for this request - =:CACHE_HIT= :: A response was generated from the cache with no requests sent upstream. - =:CACHE_MISS= :: The response came from an upstream server. - =:CACHE_MODULE_RESPONSE= :: The response was generated directly by the caching module. - =:VALIDATED= :: The response was generated from the cache after validating the entry with the origin server. * Authentication :PROPERTIES: :CUSTOM_ID: h-87f38469-36b4-44c6-ae74-0d8f5e80c2ed :END: ** Basic Auth :PROPERTIES: :CUSTOM_ID: h-d3ea348f-88ed-4193-bb16-d8d5accdc2aa :END: #+BEGIN_SRC clojure (client/get "http://example.com/protected" {:basic-auth ["user" "pass"]}) (client/get "http://example.com/protected" {:basic-auth "user:pass"}) #+END_SRC ** Digest Auth :PROPERTIES: :CUSTOM_ID: h-d1904589-e71e-43db-8b93-0f94ccecaabe :END: #+BEGIN_SRC clojure (client/get "http://example.com/protected" {:digest-auth ["user" "pass"]}) #+END_SRC ** NTLM Auth :PROPERTIES: :CUSTOM_ID: h-AE80FFDC-2016-4883-9512-2BE16640339D :END: #+BEGIN_SRC clojure (client/get "http://example.com/protected" {:ntlm-auth ["user" "pass" "host" "domain"]}) #+END_SRC ** oAuth2 :PROPERTIES: :CUSTOM_ID: h-dd077440-a1de-437e-b34e-5d6d0d1da4bd :END: #+BEGIN_SRC clojure (client/get "http://example.com/protected" {:oauth-token "secret-token"}) #+END_SRC * Advanced Usage :PROPERTIES: :CUSTOM_ID: h-d52ca837-a575-402f-81fe-53241d85f2db :END: ** Raw Request :PROPERTIES: :CUSTOM_ID: h-0d2eadbf-c1ad-4514-a932-9d173582a790 :END: A more general =request= function is also available, which is useful as a primitive for building higher-level interfaces: #+BEGIN_SRC clojure (defn api-action [method path & [opts]] (client/request (merge {:method method :url (str "http://example.com/" path)} opts))) #+END_SRC *** Boolean options :PROPERTIES: :CUSTOM_ID: h-a37c718c-43bb-43ce-936a-21ef65147295 :END: Since 0.9.0, all boolean options can be expressed as either ={:debug true}= or ={:debug? true}=, with or without the question mark. ** Persistent Connections :PROPERTIES: :CUSTOM_ID: h-4e9f116d-c293-4a0c-8e11-435c440bfe97 :END: clj-http can use persistent connections to speed up connections if multiple connections are being used: #+BEGIN_SRC clojure (with-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10} (get "http://example.org/1") (post "http://example.org/2") (get "http://example.org/3") ... (get "http://example.org/999")) #+END_SRC For async request, you can use =with-async-connection-pool= #+BEGIN_SRC clojure (with-async-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10} (get "http://example.org/1" {:async? true} resp1 exce1) (post "http://example.org/2" {:async? true} resp2 exce2) (get "http://example.org/3" {:async? true} resp3 exce3) ... (get "http://example.org/999" {:async? true} resp999 exce999)) #+END_SRC This is MUCH faster than sequentially performing all requests, because a persistent connection can be used instead creating a new connection for each request. If you want to start an async request in the =respond= callback of an async request and reuse the pool context, just use =reuse-pool=. #+BEGIN_SRC clojure (with-async-connection-pool {:timeout 5 :threads 4 :insecure? false :default-per-route 10} (get "http://example.org/1" {:async? true} resp1 exce1) (post "http://example.org/2" {:async? true} (fn [resp] (get "http://example.org/3" (reuse-pool {:async? true} resp) resp3 exce3)) exce2)) #+END_SRC There are many advanced options available when creating asynchronous connection pools that can be configured by passing an =:io-config= map in the connection manager parameters. It supports: - =:connect-timeout= - =:interest-op-queued= - =:io-thread-count= - =:rcv-buf-size= - =:select-interval= - =:shutdown-grace-period= - =:snd-buf-size= - =:so-keep-alive= - =:so-linger= - =:so-timeout= - =:tcp-no-delay= See the docstring on =with-async-connection-pool= for more information about these options. If you would prefer to handle managing the connection manager yourself, you can create a connection manager and specify it for each request: #+BEGIN_SRC clojure (def cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 2 :threads 3})) (def cm2 (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 10 :threads 1})) (get "http://example.org/1" {:connection-manager cm2}) (post "http://example.org/2" {:connection-manager cm}) (get "http://example.org/3" {:connection-manager cm2}) ;; Don't forget to shut it down when you're done! (clj-http.conn-mgr/shutdown-manager cm) (clj-http.conn-mgr/shutdown-manager cm2) #+END_SRC See the docstring on =make-reusable-conn-manager= for options and default values. In the current version, pooled async request CANNOT specify connection manager. ** Re-using =HttpClient= between requests :PROPERTIES: :CUSTOM_ID: h-b79b07fb-d024-49a2-a7f7-53863d1b8d6d :END: In some cases, you may want to re-use the same =HttpClient= object between requests, either so you don't have to build it every time, or because you make some configuration change to the request. clj-http will return the built HTTP client in =:http-client= which you can then specify in subsequent requests (with =:http-client=). Note that in order to reuse the client a connection manager must be used. #+BEGIN_SRC clojure ;; Re-use the HttpClient clj-http builds for you: (let [cm (conn/make-reusable-conn-manager {}) resp (client/get "http://example.com" {:connection-manager cm}) hclient (:http-client resp)] (client/get "http://example.com/1" {:connection-manager cm :http-client hclient}) (client/get "http://example.com/2" {:connection-manager cm :http-client hclient}) (client/get "http://example.com/3" {:connection-manager cm :http-client hclient})) ;; You can also build your own, using clj-http's helper or manually building it: (let [cm (conn/make-reusable-conn-manager {}) hclient (core/build-http-client {} false cm)] (client/get "http://example.com/1" {:connection-manager cm :http-client hclient}) (client/get "http://example.com/2" {:connection-manager cm :http-client hclient}) (client/get "http://example.com/3" {:connection-manager cm :http-client hclient})) ;; Async http clients may also be created and re-used: (let [acm (conn/make-reuseable-async-conn-manager {}) ahclient (core/build-async-http-client {} acm)] (client/get "http://example.com/1" {:connection-manager cm :http-client ahclient} handle-response handle-failure) (client/get "http://example.com/2" {:connection-manager cm :http-client ahclient} handle-response handle-failure) (client/get "http://example.com/3" {:connection-manager cm :http-client ahclient} handle-response handle-failure)) #+END_SRC ** Proxies :PROPERTIES: :CUSTOM_ID: h-49f9ca81-0bad-4cd8-87ac-c09a85fa5500 :END: A proxy can be specified by setting the Java properties: =.proxyHost= and =.proxyPort= where == is the client scheme used (normally 'http' or 'https'). =http.nonProxyHosts= allows you to specify a pattern for hostnames which do not require proxy routing - this is shared for all schemes. Additionally, per-request proxies can be specified with the =proxy-host= and =proxy-port= options (this overrides =http.nonProxyHosts= too): #+BEGIN_SRC clojure (client/get "http://example.com" {:proxy-host "127.0.0.1" :proxy-port 8118}) #+END_SRC You can also specify the =proxy-ignore-hosts= parameter with a list of hosts where the proxy should be ignored. By default this list is =#{"localhost" "127.0.0.1"}=. A SOCKS proxy can be used by creating a proxied connection manager with =clj-http.conn-mgr/make-socks-proxied-conn-manager=. Then using that connection manager in the request. For example if you wanted to connect to a local socks proxy on port =8081= you would: #+BEGIN_SRC clojure (ns foo.bar (:require [clj-http.client :as client] [clj-http.conn-mgr :as conn-mgr])) (client/get "https://google.com" {:connection-manager (conn-mgr/make-socks-proxied-conn-manager "localhost" 8081)}) #+END_SRC If your SOCKS connection requires a keystore / trust-store, you can specify that too: #+BEGIN_SRC clojure (ns foo.bar (:require [clj-http.client :as client] [clj-http.conn-mgr :as conn-mgr])) (client/get "https://google.com" {:connection-manager (conn-mgr/make-socks-proxied-conn-manager "localhost" 8081 {:keystore "/path/to/keystore.ks" :keystore-type "jks" ; default: jks :keystore-pass "secretpass" :trust-store "/path/to/trust-store.ks" :trust-store-type "jks" ; default jks :trust-store-pass "trustpass"})}) #+END_SRC You can also store the proxied connection manager and reuse it later. ** Custom Middleware :PROPERTIES: :CUSTOM_ID: h-c51cba6c-5c1b-4941-93c3-f769bb533562 :END: Sometime it is desirable to run a request with some middleware enabled and some left out, the =with-middleware= method provides this functionality: #+BEGIN_SRC clojure (with-middleware [#'clj-http.client/wrap-method #'clj-http.client/wrap-url #'clj-http.client/wrap-exceptions] (get "http://example.com") (post "http://example.com/foo" {:body (.getBytes "foo")})) #+END_SRC To see available middleware, check the =clj-http.client/default-middleware= var, which is a vector of the default middleware that clj-http uses. =clj-http.client/*current-middleware*= is bound to the current list of middleware during request time. ** Modifying Apache-specific features of the =HttpClientBuilder= and =HttpAsyncClientBuilder= :PROPERTIES: :CUSTOM_ID: h:844f078c-531e-445e-b7ce-76092bcc9928 :END: While clj-http tries to provide the features needed, there are times when it does not provide access to a parameter that you need. In these cases, you can use a couple of advanced parameters to provide arbitrary configuration functions to be run on the =HttpClientBuilder= by specifying =:http-builder-fns= and =:async-http-builder-fns=. Each of these variables is a sequence of functions of two arguments, the http builder (=HttpClientBuilder= for =:http-builder-fns= and =HttpAsyncClientBuilder= for =:async-http-builder-fns=) and the request map. #+BEGIN_SRC clojure ;; A function that takes a builder and disables Apache's cookie management (defun my-cookie-disabler [^HttpClientBuilder builder request] (when (:disable-cookies request) (.disableCookieManagement builder))) ;; The functions to modify the builder are passed in (http/post "http://www.example.org" {:http-builder-fns [my-cookie-disabler] :disable-cookies true}) #+END_SRC The functions are run in the order they are passed in (inside a =doseq=). By specifying =:http-client-builder=, your own instance of =HttpClientBuilder= will be used. A supplied =HttpClientBuilder= which sets the connection manager, redirect strategy, retry handler, route planner, cache, or cookie spec registry may find these overridden by clj-http's =:connection-manager=, =:redirect-strategy=, =:retry-handler=, =:cache=, or =:cookie-policy-registry= or =:cookie-spec=, respectively. ** Incrementally JSON Parsing :PROPERTIES: :CUSTOM_ID: h:b01c16e8-7179-468e-8890-316939ec0e38 :END: [[https://github.com/dakrone/cheshire][cheshire]] supports incrementally parsing JSON using lazy sequences. This approach can useful for processing large top-level JSON arrays because it doesn't require upfront work consuming the entire stream. #+begin_src clojure (require '[cheshire.core :as json]) (defn print-all-pokemon-names [pokemons] (for [pokemon pokemons] (println (get-in pokemon [:name :english])))) (let [url "https://raw.githubusercontent.com/fanzeyi/pokemon.json/master/pokedex.json" response (get url {:as :reader})] (with-open [reader (:body response)] ; closes the underlying connection when we're done (let [pokemons (json/parse-stream reader true)] ; You must perform all reads from the stream inside `with-open`, ; any , any lazy (doall (print-all-pokemon-names pokemons))))) #+end_src Keep in mind that the =reader= object wraps a HTTP connection. The user needs to be aware of two things: 1. The user should close the reader after processing the stream, otherwise the underlying HTTP Connection may leak and create subtle bugs. Clojure's [[https://clojuredocs.org/clojure.core/with-open][with-open]] is useful here. 2. You should realize any lazy sequences before closing the connection. Use [[https://clojuredocs.org/clojure.core/doall][doall]] or [[https://clojure.org/reference/transducers][transducers]] to prevent bugs from lazy IO. See [[https://stuartsierra.com/2015/08/25/clojure-donts-lazy-effects][Clojure Don'ts: Lazy Effects]]. In previous versions of =clj-http= (<= 3.10.0), =clj-http= defaulted to lazily parsing JSON, but this was slow and also confused users who didn't expect laziness. ** DNS Resolution Users may add their own DNS resolver function to override the default DNS Resolver. This is useful in situations where you are unable to change the name to IP Address mapping. It is analogous to the =--resolve= flag present in =curl=. This example uses =org.apache.http.impl.conn.InMemoryDnsResolver= to resolve =example.com= to IP Address =127.0.0.1=. #+BEGIN_SRC clojure (client/get "https://example.com" {:dns-resolver (doto (InMemoryDnsResolver.) (.add "example.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))}) #+END_SRC This option is supported for all of the connection managers. The =dns-resolver= can be any instance of =DnsResolver=. Here is an example of a custom implementation that attempts to look up the hostname in the supplied map and falls back to the default SystemDnsResolver if not found. Note how IPV6 addresses are specified. #+BEGIN_SRC clojure (defn custom-dns-resolver [host-map] (let [system-dns-resolver (org.apache.http.impl.conn.SystemDefaultDnsResolver.)] (reify org.apache.http.conn.DnsResolver (^"[Ljava.net.InetAddress;" resolve [this ^String host] (if-let [address (get host-map host)] (into-array [(java.net.InetAddress/getByAddress host (byte-array address))]) (.resolve system-dns-resolver host)))))) (client/get "https://example.com" {:dns-resolver (custom-dns-resolver {"example.com" [127 0 0 1] "www.google.com" [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]})}) #+END_SRC * Development :PROPERTIES: :CUSTOM_ID: h-65bbf017-2e8b-4c43-824b-24b89cc27a70 :END: Please send a pull request or open an issue if you have any problems. See =CONTRIBUTING.md= for more information. ** Faking Responses :PROPERTIES: :CUSTOM_ID: h-c3d9c7e0-cc3f-47bf-91e3-b12567b08eb6 :END: If you need to fake clj-http responses (for things like testing and such), check out the [[https://github.com/myfreeweb/clj-http-fake][clj-http-fake]] library. ** Optional Dependencies :PROPERTIES: :CUSTOM_ID: h-f1fbdad3-cf40-41e0-8ae0-8716419be228 :END: In 2.0.0+ clj-http's optional dependencies at excluded by default, in order to use the features you will need to add them to your =project.clj= file. clj-http currently has four optional dependencies, =cheshire=, =crouton=, =tools.reader= and =ring/ring-codec=. Any number of them may be included by adding them with the clj-http dependency in your project.clj: #+BEGIN_SRC clojure ;; optional dependencies [cheshire] ;; for :as :json [crouton] ;; for :decode-body-headers [org.clojure/tools.reader] ;; for :as :clojure [ring/ring-codec] ;; for :as :x-www-form-urlencoded #+END_SRC Prior to 2.0.0, you can /exclude/ the dependencies and clj-http will work without them. ** clj-http-lite :PROPERTIES: :CUSTOM_ID: h-ba6b263b-74a5-40f3-afc1-b0d785554c2b :END: Like clj-http but need something more lightweight without as many external dependencies? Check out [[https://github.com/hiredman/clj-http-lite][clj-http-lite]] for a project that can be used as a drop-in replacement for clj-http. ** Troubleshooting :PROPERTIES: :CUSTOM_ID: h-c543201e-a0e5-4e84-8eb2-6bf3e21a3140 :END: *** VerifyError class org.codehaus.jackson.smile.SmileParser overrides final method getBinaryValue... :PROPERTIES: :CUSTOM_ID: h-c3a8ebc3-a247-4327-8b71-0097d1380873 :END: This is actually caused by your project attempting to use [[https://github.com/mmcgrana/clj-json/][clj-json]] and [[https://github.com/dakrone/cheshire][cheshire]] in the same classloader. You can fix the issue by either not using clj-json (and thus choosing cheshire), or specifying an exclusion for clj-http in your project like this: #+BEGIN_SRC clojure (defproject foo "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.3.0"] [clj-http "0.3.4" :exclusions [cheshire]]]) #+END_SRC Note that if you exclude cheshire, json decoding of response bodies and json encoding of form-params cannot happen, you are responsible for your own encoding/decoding. As of clj-http 0.3.5, you should no longer see this, as Cheshire 3.1.0 and clj-json can now live together without causing problems. *** NoHttpResponseException ... due to stale connections** :PROPERTIES: :CUSTOM_ID: h-9d7cf050-ed5b-4d23-8b02-97a9b9c94737 :END: Persistent connections kept alive by the connection manager become stale: the target server shuts down the connection on its end without HttpClient being able to react to that event, while the connection is being idle, thus rendering the connection half-closed or 'stale'. This can be solved by using (with-connection-pool) as described in the 'Using Persistent Connection' section above. * Tests :PROPERTIES: :CUSTOM_ID: h-a52feb3d-d966-4287-a07e-ad7aa7918fd5 :END: To run the tests: #+BEGIN_SRC $ lein deps $ lein test Run all tests (including integration): $ lein test :all Run tests against all clojure versions $ lein all test $ lein all test :all #+END_SRC * Testimonials :PROPERTIES: :CUSTOM_ID: h-3044d1f7-6772-43c2-9ded-8c71c7f9ada2 :END: With over [[https://clojars.org/clj-http][three million]] downloads, clj-http is a widely used, battle-tested clojure library. It is also included in other libraries (like database clients) as a low-level http wrapper. Libraries using clj-http: - [[https://github.com/mattrepl/clj-oauth][clj-oauth]] - [[https://github.com/clojurewerkz/elastisch][elasticsearch]] - [[https://github.com/olauzon/capacitor][influxdb]] Libraries inspired by clj-http: - [[https://github.com/mpenet/jet][jet]] - [[https://github.com/hiredman/clj-http-lite][clj-http-lite]] * Other Libraries Providing Middleware :PROPERTIES: :CUSTOM_ID: other-middleware :END: - [[https://github.com/sharetribe/aws-sig4][aws-sig4]] :: a pure clojure implementation of AWS v4 signature request signing as middleware (feel free to open a PR or issue if you'd like to add middleware here) * License :PROPERTIES: :CUSTOM_ID: h-2de3db75-7a1b-42b8-ad3b-6ef27fc2a5ea :END: Released under the MIT License: # Local Variables: # fill-column: 100 # End: clj-http-3.12.3/changelog.org000066400000000000000000000372441407075231000157760ustar00rootroot00000000000000#+TITLE: clj-http changelog #+AUTHOR: Lee Hinman #+STARTUP: fold nodlcheck lognotestate hideall #+OPTIONS: H:4 num:nil toc:t \n:nil @:t ::t |:t ^:{} -:t f:t *:t #+OPTIONS: skip:nil d:(HIDE) tags:not-in-toc #+PROPERTY: header-args :results code :exports both :noweb yes #+HTML_HEAD: #+LANGUAGE: en * Changelog List of user-visible changes that have gone into each release ** 3.12.4 (unreleased) ** 3.12.3 - Allow http-client re-use in async situation (#599) https://github.com/dakrone/clj-http/pull/599 ** 3.12.2 - Upgrade Dependencies (#598) https://github.com/dakrone/clj-http/pull/598 ** 3.12.1 - Bugfix for :normalize-uri (#584) https://github.com/dakrone/clj-http/pull/584 ** 3.12.0 - Create SSLContext consistently for all connection managers (#575) https://github.com/dakrone/clj-http/pull/575 - Adds RequestConfig Option :normalize-uri (#583) https://github.com/dakrone/clj-http/pull/583 ** 3.11.0 - Adds workaround for Async Multipart uploads greater than 25 kb (#574) https://github.com/dakrone/clj-http/pull/574 - Adds an additional style for multi-param-style added (#562) https://github.com/dakrone/clj-http/pull/562 - Close transit input stream after reading response (#565) https://github.com/dakrone/clj-http/pull/565 - Bump patch versions of apache httpcomponents to latest. (#569) https://github.com/dakrone/clj-http/pull/569 - Fixed decode-json-body (#568) https://github.com/dakrone/clj-http/pull/568 - Handle quoted parameter values in content type (#573) https://github.com/dakrone/clj-http/pull/573 ** 3.10.3 - Improve error message when using incompatible version of cheshire https://github.com/dakrone/clj-http/pull/558 - Properly handle "308 Permanent Redirect" status code https://github.com/dakrone/clj-http/pull/554 ** 3.10.2 - Fix performance regressions from #528 https://github.com/dakrone/clj-http/pull/546 - Adds support for custom DNS Resolvers https://github.com/dakrone/clj-http/pull/545 - Buffer :debug output to improve readability https://github.com/dakrone/clj-http/pull/544 - Improve compatbility with GraalVM https://github.com/dakrone/clj-http/pull/543 - Bug fix: Check first byte before wrapping response stream with gunzip https://github.com/dakrone/clj-http/pull/549 ** 3.10.1 - JSON parsing is always strict. See [[file:README.org::*Incrementally%20JSON%20Parsing][README#Incrementally JSON Parsing]]. This is a *breaking change* and users *must* upgrade to cheshire >= 5.9.0. https://github.com/dakrone/clj-http/pull/507 ** 3.10.0 - Add trust-manager and key-managers support to the client https://github.com/dakrone/clj-http/pull/469 - Improving consistency of connection option names https://github.com/dakrone/clj-http/pull/483 https://github.com/dakrone/clj-http/issues/477 - Ensure Socket Timeout is set for BasicHttpClientConnectionManager https://github.com/dakrone/clj-http/pull/463 - Reduce body allocation and copying https://github.com/dakrone/clj-http/pull/475 ** 3.9.1 - Fix body parsing when first byte value is 255 https://github.com/dakrone/clj-http/pull/449 - Add custom =:unexceptional-status= option https://github.com/dakrone/clj-http/pull/451 ** 3.9.0 - Add support for reusable http clients, returning the client in =:http-client= and allowing one to be specified (with the same setting) - https://github.com/dakrone/clj-http/issues/441 - Cancelling the =Future= returned from an async http request now also aborts the HttpRequest object - Async connection managers no longer put the connection manager in an illegal ACTIVE state [[https://github.com/dakrone/clj-http/issues/443][#443]] - Added the =:cookie-spec= and =:cookie-policy-registry= options for specifying a custom cookie spec for parsing cookies. Since clj-http doesn't rely on Apache's cookies handling, this is for advanced users who wish to add their own cookie validation, or use Apache's handling instead of clj-http's. It also allows a user who wants to registry a custom spec to reuse the spec without creating it for every request. Semi-related to https://github.com/dakrone/clj-http/issues/444 - Added support for caching HTTP responses from a server. This can dramatically speed up requests to the same URL. Filling and invalidating the cache is handled by Apache's httpclient-cache project, with configuration exposed under the =:cache= and =:cache-config= parameters in the option map. https://github.com/dakrone/clj-http/issues/445 ** 3.8.0 - Reintroduce the =:save-request= and =:debug-body= options - +Wrap nested querystring params before form params, fixing https://github.com/dakrone/clj-http/issues/427+ Reverted, see further below - Merged https://github.com/dakrone/clj-http/pull/426 to allow an empty SSLGenericSocketFactory context - Merged https://github.com/dakrone/clj-http/pull/424 to add :mime-subtype request parameter to override mime subtype - create-multipart-entity with three arguments arity lets the selection of =HttpMultipartMode= - new request key :http-multipart-mode which is HttpMultipartMode/STRICT by default - Added =:ignore-nested-query-string=, =:flatten-nested-form-params=, and =:flatten-nested-keys= options for finer-grained control over which nested parts of the request are flattened. Fixes https://github.com/dakrone/clj-http/issues/427 - Added =:http-builder-fns= and =:async-http-builder-fns= to support arbitrary customizations to the =HttpClientBuilder= and =HttpAsyncClientBuilder= - Fixed an issue where redirects to a bad location could cause the async client to hang - https://github.com/dakrone/clj-http/pull/435 - =client/parse-url= now includes the original URL in the =:url= key - =core/get-cookie-policy= is now a multimethod. This allows users to customize the return of their own cookie validation method. - Empty responses with coercion no longer throw exceptions when processing empty gzipped response streams. Fixes https://github.com/dakrone/clj-http/issues/257 ** 3.7.0 This list contains all the changes since 3.0.0. Added: - HttpRequestInterceptor support 155bd23 - protocol-version and reason-phrase f430517 - support for async HTTP requests (like Ring) 44d10ec - support for different multi-param encoding (:repeating, :array, :indexed) cddeb3e - Add unparse function aec7dd1 - Added :redirect-strategy :graceful - Allow RequestConfig and HttpClientContext to be injected feb3c48 Removed: - :save-request Changed: - re-written middleware using apache http client 4.5 - Fix retry-handler to be added in correct place a2c31f5 - POST Mutipart: Use charset "UTF-8" instead of "ASCII" as default charset to support internationalization 983508f ** 2.0.0 - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it supports Clojure 1.7.0 correctly - merged https://github.com/dakrone/clj-http/pull/264 to add support for coercion of urlencoded data - make ALL optional dependencies opt-in, rather than opt-out ** 1.1.2 - bumped dependencies for transit-clj and tools.reader - merge https://github.com/dakrone/clj-http/pull/263 to only decode body headers when the content-type is either missing or starts with "text" ** 1.1.1 - merge https://github.com/dakrone/clj-http/pull/262 to prevent NullPointerException when decoding body headers with HEAD requests - merge https://github.com/dakrone/clj-http/pull/261 to decode user info from URL if provided - merge https://github.com/dakrone/clj-http/pull/260 to upgrade tools.reader for better cljs compatibility - add =304= (not modified) to the list of unexceptional responses, see #259 ** 1.1.0 - merged https://github.com/dakrone/clj-http/pull/255 to add support for Windows NTLM authentication - Add the `with-additional-middleware` macro - Add the ability to specify form-param-encoding for encoding form parameters - merged https://github.com/dakrone/clj-http/pull/248 to removed deprecated cookie APIs from cookie.clj - merged https://github.com/dakrone/clj-http/pull/245 to do some cleanups and small import fixes - merged https://github.com/dakrone/clj-http/pull/240 to implement meta/with-meta for the header map - merged https://github.com/dakrone/clj-http/pull/242 fixing a connection leak when http-entity is null - bumped all dependencies to latest versions - merged https://github.com/dakrone/clj-http/pull/235 to fix wrap-nested-params - merged https://github.com/dakrone/clj-http/pull/236 to clean up multipart constructors and reflection - merged https://github.com/dakrone/clj-http/pull/234 to allow scheme customization in default connection ** 1.0.1 - merged https://github.com/dakrone/clj-http/pull/232 to fix =empty= on header-map - fix :json-strict-string-keys - exclude clojure.core/update from client ns - added =:decode-cookies= option to allow skipping cookie header decode (if the server sends incorrectly formatted cookies for some reason) ** 1.0.0 - merged https://github.com/dakrone/clj-http/pull/215 to add transit support - drop support for clojure 1.4.0, start testing 1.7.0 - merged https://github.com/dakrone/clj-http/pull/213 to allow passing in an already existing keystore, not just a path - merged https://github.com/dakrone/clj-http/pull/211 to detect charset encoding for url-encode ** 0.9.2 - merged https://github.com/dakrone/clj-http/pull/206 to handle null passwords for keystores - merged https://github.com/dakrone/clj-http/pull/201 to make :auto content type parsing dispatch pluggable - Bump crouton and tools.reader dependencies - Merged https://github.com/dakrone/clj-http/pull/199 to add support for form parameters in the PATCH method - Bump dependencies and fix tests for 1.6.0 compatibility ** 0.9.1 - automatically coerce header values to strings - fix issue where :ignore-unknown-host wasn't using the =opt= function correctly ** 0.9.0 - Bumped httpcore to 4.3.2 - Merged https://github.com/dakrone/clj-http/pull/190 to support file multiparts with content, mime-type and name - Unify all boolean operators so {:debug true} and {:debug? true} are treated the same - Fix :trace-redirects being [nil] when :uri is used - Merged https://github.com/dakrone/clj-http/pull/184 containing a bevy of changes: - initial header-map implementation, allowing headers to be used case insensitively - drop support for clojure 1.2 and 1.3 - add support for clojure 1.6 - change all :use statements to :require statements - use better docstring support for defs - remove sleep calls in tests - make Jetty quieter while running tests - newer type hinting syntax ** 0.7.9 - Make :decode-body-headers more reliable by using a byte array instead of slurp. - Merged https://github.com/dakrone/clj-http/pull/181 to fix some tests - Merged https://github.com/dakrone/clj-http/pull/178 to eliminate test reflection - Merged https://github.com/dakrone/clj-http/pull/177 to update apache HTTP deps - Merged https://github.com/dakrone/clj-http/pull/175 to add {:as :json-strict} for output coercion - Added {:as :json-strict-string-keys} output coercion - bump dependencies to their latest - Merged https://github.com/dakrone/clj-http/pull/172 to update .gitignore file and clean up whitespace for new clojure-mode - Merged https://github.com/dakrone/clj-http/pull/171 to support SOCKS proxies * Work log ** 2015-07-24 - branched master to create 2.x - start major rewrite on master branch for non-deprecated Apache usage ** Released 2.0.0 ** 2015-07-18 - merged https://github.com/dakrone/clj-http/pull/274 to update Potemkin so it supports Clojure 1.7.0 correctly ** 2015-05-23 - merged https://github.com/dakrone/clj-http/pull/264 to add support for coercion of urlencoded data - make ALL optional dependencies opt-in, rather than opt-out ** Released 1.1.2 ** 2015-05-06 - bumped dependencies for transit-clj and tools.reader ** 2015-04-24 - merge https://github.com/dakrone/clj-http/pull/263 to only decode body headers when the content-type is either missing or starts with "text" ** Released 1.1.1 ** 2015-04-22 - merge https://github.com/dakrone/clj-http/pull/262 to prevent NullPointerException when decoding body headers with HEAD requests ** 2015-04-20 - merge https://github.com/dakrone/clj-http/pull/261 to decode user info from URL if provided ** 2015-04-14 - merge https://github.com/dakrone/clj-http/pull/260 to upgrade tools.reader for better cljs compatibility ** 2015-04-05 - add =304= (not modified) to the list of unexceptional responses, see #259 ** Released 1.1.0 ** 2015-03-03 - merged https://github.com/dakrone/clj-http/pull/255 to add support for Windows NTLM authentication ** 2015-02-08 - Add the `with-additional-middleware` macro - Add the ability to specify form-param-encoding for encoding form parameters ** 2015-01-19 - merged https://github.com/dakrone/clj-http/pull/248 to removed deprecated cookie APIs from cookie.clj - merged https://github.com/dakrone/clj-http/pull/245 to do some cleanups and small import fixes ** 2015-01-15 - merged https://github.com/dakrone/clj-http/pull/240 to implement meta/with-meta for the header map - merged https://github.com/dakrone/clj-http/pull/242 fixing a connection leak when http-entity is null - bumped all dependencies to latest versions ** 2014-12-13 - merged https://github.com/dakrone/clj-http/pull/235 to fix wrap-nested-params ** 2014-12-12 - merged https://github.com/dakrone/clj-http/pull/236 to clean up multipart constructors and reflection ** 2014-12-02 - merged https://github.com/dakrone/clj-http/pull/234 to allow scheme customization in default connection ** Released 1.0.1 ** 2014-10-28 - merged https://github.com/dakrone/clj-http/pull/232 to fix =empty= on header-map ** 2014-10-17 - fix :json-strict-string-keys ** 2014-09-08 - exclude clojure.core/update from client ns ** 2014-08-15 - added =:decode-cookies= option to allow skipping cookie header decode (if the server sends incorrectly formatted cookies for some reason) ** Released 1.0.0 ** 2014-08-11 - merged https://github.com/dakrone/clj-http/pull/215 to add transit support - drop support for clojure 1.4.0, start testing 1.7.0 ** 2014-08-07 - merged https://github.com/dakrone/clj-http/pull/213 to allow passing in an already existing keystore, not just a path ** 2014-07-27 - merged https://github.com/dakrone/clj-http/pull/211 to detect charset encoding for url-encode ** Released 0.9.2 ** 2014-05-27 - merged https://github.com/dakrone/clj-http/pull/206 to handle null passwords for keystores ** 2014-05-14 - merged https://github.com/dakrone/clj-http/pull/201 to make :auto content type parsing dispatch pluggable ** 2014-04-21 - Bump crouton and tools.reader dependencies ** 2014-04-09 - Merged https://github.com/dakrone/clj-http/pull/199 to add support for form parameters in the PATCH method ** 2014-03-26 - Bump dependencies and fix tests for 1.6.0 compatibility ** Released 0.9.1 ** 2014-03-15 - automatically coerce header values to strings ** 2014-03-05 - fix issue where :ignore-unknown-host wasn't using the =opt= function correctly ** Released 0.9.0 ** 2014-02-25 - Bumped httpcore to 4.3.2 ** 2014-02-19 - Merged https://github.com/dakrone/clj-http/pull/190 to support file multiparts with content, mime-type and name ** 2014-02-16 - Unify all boolean operators so {:debug true} and {:debug? true} are treated the same ** 2014-02-09 - Fix :trace-redirects being [nil] when :uri is used ** 2014-02-06 - Merged https://github.com/dakrone/clj-http/pull/184 containing a bevy of changes: - initial header-map implementation, allowing headers to be used case insensitively - drop support for clojure 1.2 and 1.3 - add support for clojure 1.6 - change all :use statements to :require statements - use better docstring support for defs - remove sleep calls in tests - make Jetty quieter while running tests - newer type hinting syntax ** Released 0.7.9 ** 2014-02-01 - Make :decode-body-headers more reliable by using a byte array instead of slurp. clj-http-3.12.3/changelog.org_archive000066400000000000000000000575761407075231000175110ustar00rootroot00000000000000 Archived entries from file /Users/hinmanm/src/clj/clj-http/changelog.org * Archived Tasks ** 0.7.8 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - Added the `proxy-ignore-hosts` option to allow specifying a list of hosts where a proxy should be ignored - merged https://github.com/dakrone/clj-http/pull/166 to fix some small whitespace and reflection stuff - Close the body of a response in wrap-redirects since all bodies are streams now. Otherwise, the body is kept open indefinitely. ** 0.7.7 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/162 to pass through json opts for form-param encoding - bumped dependencies - fix #159 - issue with :decode-body-headers introduced with streaming bodies - merged https://github.com/dakrone/clj-http/pull/156 to add `:raw-headers` option to return an additional untouched :raw-headers map - merged https://github.com/dakrone/clj-http/pull/154 to handle query-params not clobbering query-params in the URL string - bump main deps - merged https://github.com/dakrone/clj-http/pull/151 to prevent shutting down a reusable connection manager when an error occurs ** 0.7.6 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - add logging config for local testing only - remove "content-encoding" header if the body is automatically decompressed to allow for pass-through. If header is removed, assoc :orig-content-encoding to response map. - merged https://github.com/dakrone/clj-http/pull/149 to fix closing the stream when coerced to byte array - merged https://github.com/dakrone/clj-http/pull/146 to correctly reference parameter names ** 0.7.5 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - Only redirect if a "location" header is actually, present, avoiding an NPE in the event it's missing. (fixes #145) ** 0.7.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/143 for fixing some weirdness around body streams and inflation - streams everywhere, all bodies coming out of core.clj are now streams, so {:as :stream} truly streams the response, keeping it out of memory - remove some more reflection ** 0.7.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - correctly close single client connection manager if {:as :stream} is used, fixes #142 - merged https://github.com/dakrone/clj-http/pull/138 to preserve http method for 307 redirect - merged in parse-url parameters into follow-redirect so request map is not inconsistent - bumped http* deps to 4.2.5 - fixed cookie compact-map not to remove falsey values, only nil ones - merged https://github.com/dakrone/clj-http/pull/135 to fix discard always defaulting to true - add *current-middleware* to see available middleware during a with-middleware request (for nesting) ** 0.7.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/127 to allow custom cookie policies - allow specifying :length for mulitpart inputstream bodies to avoid chunked transfer encoding - bumped cheshire to 5.1.1 - merged https://github.com/dakrone/clj-http/pull/133 to remove some reflection ** 0.7.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - clarify :throw-exceptions in documentation - define default-middleware for use in wrap-request, remove bad all-middleware method - merged https://github.com/dakrone/clj-http/pull/130 to encode query-params - merged https://github.com/dakrone/clj-http/pull/124 to handle URL-encoding invalid characters in the URI - bump cheshire to 5.1.0 - Switch from deprecated SingleClientConnManager to BasicClientConnectionManager - merged https://github.com/dakrone/clj-http/pull/126 to bump httpcore version - bump dependencies to latest versions ** 0.7.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/122 for using *data-readers* when using tools.reader to parse EDN - fix an issue with 1.3 where *data-readers* is not available - merged https://github.com/dakrone/clj-http/pull/121 to fix auto-coercion with json - support application/edn as an auto-coercion type - add tools.reader as an optional dependency, edn/read will be used if available, otherwise read-string with *read-eval* bound to false is used. See https://github.com/dakrone/clj-http/pull/120 - Bump clojure to 1.5.1 ** 0.6.5 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - allow json coercion for exception cases based on :coerce setting, can be either :always, :exceptional or :unexceptional - Update clojure to 1.5 - Move SingleClientConnManager shutdown into finally block - bind *read-eval* to false when reading for {:as :clojure} - bump cheshire to 5.0.2 ** 0.6.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/113 to update the connection pooling code - refactor pooled connection managers to allow specifying the :connection-manager option - merged https://github.com/dakrone/clj-http/pull/112 to allow json coercion on error responses when :as :auto is used - allow redirects when :url is not set in the request - merged https://github.com/dakrone/clj-http/pull/110 to handle the case when the server-side uses deflate incorrectly - added `with-middleware` to allow running requests with a custom middleware list - added `all-middleware` var listing all the wrap-* middleware that clj-http knows of - clj-http.client/request is now marked as dynamic for rebinding ** 0.6.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - Remove wrap-cookie-store middleware, CookieStore headers are automatically added by Apache - set the SINGLE_COOKIE_HEADER value to true to ensure Apache sends only one "Cookie:" header - Do not add CookieStore or Cookie header if there are no cookies in the cookie jar ** 0.6.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/106 to remove query params for redirection. - whitespace fixes; fix test that wasn't working correctly ** 0.6.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - bump httpcore to 4.2.3 - Fix an issue (#105) related to the "Content-Length" header being automatically added to GET requests ** 0.6.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: (bumped to 0.6.0 since Cheshire has changed major versions) - Update Cheshire to 5.0.1 - Add type hint for getting headers from body (michaelklishin) ** 0.5.8 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - add buffering for HttpEntity, with ability to turn off if needed, fixes lein issue with repeatable requests ** 0.5.7 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - create a custom X509HostnameVerifier for the :insecure? option - explicitly require httpcore instead of leaving it to a transitive dep - update httpcomponents to 4.2.2 - implement HTML5 charset header reading from body ** 0.5.6 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - bump Crouton to 0.1.1 for faster speeds - add feature to decode body headers, merging them into response headers if they are present - merged https://github.com/dakrone/clj-http/pull/98 to add optional :default-per-route to with-connection-pool ** 0.5.5 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - bump cheshire to fix json encoding bug ** 0.5.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/95 to add support for setting aribtrary client params to the http client - Merged https://github.com/dakrone/clj-http/pull/94 to remove some reflection - update cheshire dep, make clojure a dev-dependency - allow overriding the multipart part name with :part-name ** 0.5.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/91 to add support for :digest-auth - added request timing middleware to add :request-time key for request timing - add wrap-cookie-store to send cookie-store cookies with a request automatically - merged https://github.com/dakrone/clj-http/pull/90 to standardize on lower-case headers for HTTP requests ** 0.5.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/88 to add chunked encoding support (=:length= no longer required along with input stream =:body=) ** 0.5.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - fix clojure 1.3's exception wrapping for some exceptions - merged https://github.com/dakrone/clj-http/pull/87 to allow using http.nonProxyHosts - mark json-encode and json-decode dynamic, so they could be rebound if desired - update httpclient and httpmime to 4.2.1 - update commons-codec to 1.6 - update common-io to 2.4 - change body decompression to be optional, if desired - make the :content-type and :character-encoding options part of middleware, not the core request - document all the middleware - merged https://github.com/dakrone/clj-http/pull/85 to allow low-level callback for debugging ** 0.5.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - rewrite multipart body entity creation to use different map format, allowing :mime-type and :encoding keys in some cases ** 0.4.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - bump cheshire to 4.0.1 and slingshot to 0.10.3 - fix an issue where cookies were encoded and should not be - merged https://github.com/dakrone/clj-http/pull/80 to allow specifying the keystore type - merged https://github.com/dakrone/clj-http/pull/79 to allow pluggable output coercion (multimethod) ** 0.4.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - support custom x509 keystore/trust-stores ** 0.4.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - fixed an issue where multiple link headers would cause an exception to be thrown ** 0.4.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - added :debug-body that adds plaintext body information to the :debug output - fix json encoded form params with nested maps - fix attempted json coercion when a bad status is received - merged https://github.com/dakrone/clj-http/pull/69 to add support for :oauth-token authentication - merged https://github.com/dakrone/clj-http/pull/70 to save the apache Http object when :save-request? is true - merged https://github.com/dakrone/clj-http/pull/68 to support additional options/delete/copy/move HTTP methods - add support for the :patch method type ** 0.4.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/66 to add support for 'Link' header - added ability to specify your own retry-handler for IOExceptions if desired - bumped httpclient and httpmime to 4.1.3 - bump to released version of clojure (1.4) - added documentation about ipv6 requests - fixed https://github.com/dakrone/clj-http/issues/57 by have wrap-redirects redirect according to the RFC and adding the :force-redirects option to be more browser-like - merged https://github.com/dakrone/clj-http/pull/61 to add support for nested param maps ** 0.3.6 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - fixed an issue where urls like http://user:pass@foo.com didn't work correctly for basic-auth - added support for cookie stores - added utility methods to retrieve cookies as a map from the cookie store - set the default maximum number of redirects to 20 ** 0.3.5 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - same as 0.3.4, but with a newer cheshire that doesn't interfere with clj-json ** 0.3.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - improved commit from pull/55 to make the predicate more generalized to any kind of entity request - make Cheshire an optional dependency, only for {:as :json} and json form-params - merged https://github.com/dakrone/clj-http/pull/55 to fix HEAD requests with body contents - merged https://github.com/dakrone/clj-http/pull/53 to add status functions into the clj-http.client namespace - added the ability to specify {:as :clojure} to get back a clojure datastructure, or {:as :auto} with content-type=application/clojure - merged https://github.com/dakrone/clj-http/pull/52 to support json-encoded form params - added a test for json-encoded form params as request body ** 0.3.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/51 to allow :form-params on PUT requests - bump Cheshire and slingshot deps - add the :throw-entire-message? option to include resp in Exception message - throw an IllegalArgumentException instead of a regulor Exception on nil urls - add ability to redirect to relative paths (ngrunwald) ** 0.3.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/48 to fix :stream bodies (to make sure they are not coerced on output) - merged https://github.com/dakrone/clj-http/pull/49 to check for nil URLs when using client functions - switch from assertions to exceptions for nil URLs - merged https://github.com/dakrone/clj-http/pull/46 to add :trace-redirects to the response map - merged https://github.com/dakrone/clj-http/pull/47 to allow GET requests with a :body set - merged https://github.com/dakrone/clj-http/pull/44 to add ability to specify maximum number of redirects - add tests for max-redirects - merged https://github.com/dakrone/clj-http/pull/42 to allow strings or keywords for :scheme in requests - added test for different :schemes ** 0.3.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/40 to allow per-request proxy settings - remove a few more reflections - added ablity to return the body as a stream with {:as :stream} - general code cleanup ** 0.3.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - add ability to ignore unknown host if desired ({:ignore-unknown-host? true}) - use much better Enitity's for the body, depending on type - bump all dependencies - test re-org to make better sense (and allow C-c t in emacs) - merged https://github.com/dakrone/clj-http/pull/36 to fix url-encoding of multiple query params using the same key - merged https://github.com/dakrone/clj-http/pull/34 to fix decoding cookies that don't follow RFC spec - Add better coercion, adding {:as :json}, {:as :json-string-keys} and {:as :auto} ** 0.2.7 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - merged https://github.com/dakrone/clj-http/pull/31 to remove more reflection warnings - some whitespace changes - merged https://github.com/dakrone/clj-http/pull/30 to remove more reflection warnings - removed swank from dev deps - bump 1.4 to alpha3 in multi deps ** 0.2.6 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - don't use :server-port unless required (fixes problem with some web servers) - smaller error message on exceptions (thrown object is still the same) - added the :save-request? option to return the request object in a :request key in the response map - multiple headers with the same name are now preserved when they have differing cases ** 0.2.5 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - multipart form uploads - bump slingshot to 0.9.0 ** 0.2.4 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - Got a functioning reusable connection method, (with-connection-pool ...) - upgrade slingshot to 0.8.0 - upgrade commons-io to 2.1 - merged https://github.com/dakrone/clj-http/pull/20 to allow :basic-auth as a string ** 0.2.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - added :insecure? flag - fix AOT by requiring clojure.pprint - wrap-redirects now handles recursive redirects ** 0.2.2 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - wrap-exceptions now uses Slingshot to throw a much more useful exception when there was a problem with the request - fixed an issue when malformed server responses could NPE the decompression middleware - added a :debug flag to pretty-print the request map and object to stdout before performing the request to aid in debugging ** 0.2.1 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - decode cookies from response into :cookies (thanks r0man) - redone redirects, they can now be toggled with {:follow-redirects false} in the request - decompression of responses has been fixed (thanks senior) - accept Content-Encoding or content-encoding from responses (thanks senior) - added ability to specify sending a url-encoded :body of form params using {:form-params {:key value}} (thanks senior) ** 0.2.0 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - updated dependencies to be the latest versions - added ability to use system proxy for connections (thanks jou4) - added ability to specify socket and connection timeouts in request (thanks zkim) ** 0.1.3 :PROPERTIES: :ARCHIVE_TIME: 2014-03-05 Wed 16:32 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Changelog :ARCHIVE_CATEGORY: changelog :END: - see: https://github.com/mmcgrana/clj-http ** Released 0.7.8 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: ** 2014-01-03 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: - bump dependencies to their latest - Merged https://github.com/dakrone/clj-http/pull/172 to update .gitignore file and clean up whitespace for new clojure-mode - Merged https://github.com/dakrone/clj-http/pull/171 to support SOCKS proxies ** 2014-01-15 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: - Merged https://github.com/dakrone/clj-http/pull/175 to add {:as :json-strict} for output coercion - Added {:as :json-strict-string-keys} output coercion ** 2014-01-21 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: - Merged https://github.com/dakrone/clj-http/pull/177 to update apache HTTP deps ** 2014-01-27 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: - Merged https://github.com/dakrone/clj-http/pull/178 to eliminate test reflection ** 2014-01-28 :PROPERTIES: :ARCHIVE_TIME: 2015-04-22 Wed 23:13 :ARCHIVE_FILE: ~/src/clj/clj-http/changelog.org :ARCHIVE_OLPATH: Work log :ARCHIVE_CATEGORY: changelog :END: - Merged https://github.com/dakrone/clj-http/pull/181 to fix some tests clj-http-3.12.3/examples/000077500000000000000000000000001407075231000151425ustar00rootroot00000000000000clj-http-3.12.3/examples/body_coercion.clj000066400000000000000000000021561407075231000204560ustar00rootroot00000000000000(ns clj-http.examples.body-coercion (:require [clj-http.client :as http] [camel-snake-kebab.core :refer [->kebab-case-keyword]])) ;; register your own body coercers by participating in the coerce-response-body multimethod ;; dispatch to it by using {:as :json-kebab-keys} as an argument to http client calls ;; this example uses camel-snake-kebab to turn a camel-cased JSON API into ;; idiomatic kebab-cased keywords in clojure data structures and is much ;; faster than applying via postwalk or similar (defmethod http/coerce-response-body :json-kebab-keys [req resp] (http/coerce-json-body req resp (memoize ->kebab-case-keyword) false)) ;; example of use; note that in the response, the first field is called userId ;; ;; (:body (http/get "http://jsonplaceholder.typicode.com/posts/1" {:as :json-kebab-keys})) ;; => ;; {:user-id 1, ;; :id 1, ;; :title "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", ;; :body "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" ;; } clj-http-3.12.3/examples/caching_middleware.clj000066400000000000000000000054121407075231000214270ustar00rootroot00000000000000(ns clj-http.examples.caching-middleware "Example middleware that caches successful GET requests using core.cache." (:require [clj-http.client :as http] [clojure.core.cache :as cache]) (:import (java.nio.charset StandardCharsets))) (def http-cache (atom (cache/ttl-cache-factory {} :ttl (* 60 60 1000)))) (defn slurp-bytes "Read all bytes from the stream. Use for example when the bytes will be in demand after stream has been closed." [stream] (.getBytes (slurp stream) StandardCharsets/UTF_8)) (defn- cached-response "Look up the response in the cache using URL as the cache key. If the cache has the response, return the cached value. If the cache does not have the response, invoke the remaining middleware functions to perform the request and receive the response. If the response is successful (2xx) and is a GET, store the response in the cache. Return the response." ([client req] (let [cache-key (str (:server-name req) (:uri req) "?" (:query-string req))] (if (cache/has? @http-cache cache-key) (do (println "CACHE HIT") (reset! http-cache (cache/hit @http-cache cache-key)) ; update cache stats (cache/lookup @http-cache cache-key)) ; return cached value ; do not invoke further middleware (do (println "CACHE MISS") (let [resp (update (client req) :body slurp-bytes)] ; middleware chain invoked (when (and (http/success? resp) (= (:request-method req) :get)) (reset! http-cache (cache/miss @http-cache cache-key resp)) ; update cache value resp))))))) (defn wrap-caching-middleware "Middleware are functions that add functionality to handlers. The argument client is a handler. This wrapper function adds response caching to the client." [client] (fn ([req] (cached-response client req)))) (defn example "Add the caching middleware and perform a GET request using the URI argument. Subsequent invocations of this function using an identical URI argument before the Time To Live expires can be expected to hit the cache." [& uri] (-> (time (http/with-additional-middleware [#'wrap-caching-middleware] (http/get (or uri "https://api.github.com") { ;; :debug true ;; :debug-body true ;; :throw-entire-message? true }))) (select-keys ,,, [:status :reason-phrase :headers]))) ;; Try this out: ;; ;; user> (use '[clj-http.examples.caching-middleware :as mw]) ;; nil ;; user> (mw/example) ;; CACHE MISS ;; "Elapsed time: 1910.027361 msecs" ;; {:status 200, :reason-phrase "OK"} ;; user> (mw/example) ;; CACHE HIT ;; "Elapsed time: 0.83484 msecs" ;; {:status 200, :reason-phrase "OK"} ;; user> clj-http-3.12.3/examples/kubernetes_pod.clj000066400000000000000000000022251407075231000206460ustar00rootroot00000000000000(:ns clj-http.examples.kubernetes-pod "This is an example of calling the Kubernetes API from inside a pod. K8s uses a custom CA so that you can authenticate the API server, and provides a token per pod so that each pod can authenticate itself with the APi server. If you are still having 401/403 errors, look carefully at the message, if it includes a ServiceAccount name, this part worked, and your problem is likely at the Role/RoleBinding level." (:require [clj-http.client :as http] [less.awful.ssl :refer [trust-store]])) ;; Note that this is not a working example, you'll need to figure out your K8s API path. (let [k8s-trust-store (trust-store (clojure.java.io/file "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")) bearer-token (format "Bearer %s" (slurp "/var/run/secrets/kubernetes.io/serviceaccount/token")) kube-api-host (System/getenv "KUBERNETES_SERVICE_HOST") kube-api-port (System/getenv "KUBERNETES_SERVICE_PORT")] (http/get (format "https://%s:%s/apis/" kube-api-host kube-api-port) {:trust-store k8s-trust-store :headers {:authorization bearer-token}})) clj-http-3.12.3/examples/logging-apache-requests.clj000066400000000000000000000021461407075231000223550ustar00rootroot00000000000000(ns clj-http.examples.logging-apache-requests "This is an example of configuring Apache's log4j2 logging from Clojure, so that the http client logging can be seen" (:require [clj-http.client :as http]) (:import (org.apache.logging.log4j Level LogManager))) ;; This is a helper function to change the log level for log4j2. If you use a ;; different logging framework (and subsequently a different bridge for log4j ;; then you'll need to substitute your own logging configuration (defn change-log-level! [logger-name level] (let [ctx (LogManager/getContext false) config (.getConfiguration ctx) logger-config (.getLoggerConfig config logger-name)] (.setLevel logger-config level) (.updateLoggers ctx))) ;; Here is an example of using it to change the root logger to "DEBUG" and the ;; back to "INFO" after a request has been completed (defn post-page-with-debug [] (change-log-level! LogManager/ROOT_LOGGER_NAME Level/DEBUG) (http/post "https://httpbin.org/post" {:body "this is a test"}) (change-log-level! LogManager/ROOT_LOGGER_NAME Level/INFO)) clj-http-3.12.3/examples/progress_download.clj000066400000000000000000000062641407075231000213770ustar00rootroot00000000000000(ns clj-http.examples.progress-download (:require [clj-http.client :as http] [clojure.java.io :refer [output-stream]]) (:import (org.apache.commons.io.input CountingInputStream))) (defn print-progress-bar "Render a simple progress bar given the progress and total. If the total is zero the progress will run as indeterminated." ([progress total] (print-progress-bar progress total {})) ([progress total {:keys [bar-width] :or {bar-width 50}}] (if (pos? total) (let [pct (/ progress total) render-bar (fn [] (let [bars (Math/floor (* pct bar-width)) pad (- bar-width bars)] (str (clojure.string/join (repeat bars "=")) (clojure.string/join (repeat pad " ")))))] (print (str "[" (render-bar) "] " (int (* pct 100)) "% " progress "/" total))) (let [render-bar (fn [] (clojure.string/join (repeat bar-width "-")))] (print (str "[" (render-bar) "] " progress "/?")))))) (defn insert-at [v idx val] "Addes value into a vector at an specific index." (-> (subvec v 0 idx) (conj val) (into (subvec v idx)))) (defn insert-after [v needle val] "Finds an item into a vector and adds val just after it. If needle is not found, the input vector will be returned." (let [index (.indexOf v needle)] (if (neg? index) v (insert-at v (inc index) val)))) (defn wrap-downloaded-bytes-counter "Middleware that provides an CountingInputStream wrapping the stream output" [client] (fn [req] (let [resp (client req) counter (CountingInputStream. (:body resp))] (merge resp {:body counter :downloaded-bytes-counter counter})))) (defn download-with-progress [url target] (http/with-middleware (-> http/default-middleware (insert-after http/wrap-redirects wrap-downloaded-bytes-counter) (conj http/wrap-lower-case-headers)) (let [request (http/get url {:as :stream}) length (Integer. (get-in request [:headers "content-length"] 0)) buffer-size (* 1024 10)] (println) (with-open [input (:body request) output (output-stream target)] (let [buffer (make-array Byte/TYPE buffer-size) counter (:downloaded-bytes-counter request)] (loop [] (let [size (.read input buffer)] (when (pos? size) (.write output buffer 0 size) (print "\r") (print-progress-bar (.getByteCount counter) length) (recur)))))) (println)))) ;; Example of progress bar output (sample steps) ;; ;; [=== ] 7% 2094930/26572400 ;; [============================== ] 60% 16062930/26572400 ;; [========================================= ] 83% 22290930/26572400 ;; [==================================================] 100% 26572400/26572400 ;; ;; In case the content-length is unknown, the bar will be displayed as: ;; ;; [--------------------------------------------------] 4211440/? clj-http-3.12.3/project.clj000066400000000000000000000055261407075231000154740ustar00rootroot00000000000000(defproject clj-http "3.12.3" :description "A Clojure HTTP library wrapping the Apache HttpComponents client." :url "https://github.com/dakrone/clj-http/" :license {:name "The MIT License" :url "http://opensource.org/licenses/mit-license.php" :distribution :repo} :global-vars {*warn-on-reflection* false} :min-lein-version "2.0.0" :exclusions [org.clojure/clojure] :dependencies [[org.apache.httpcomponents/httpcore "4.4.14"] [org.apache.httpcomponents/httpclient "4.5.13"] [org.apache.httpcomponents/httpclient-cache "4.5.13"] [org.apache.httpcomponents/httpasyncclient "4.1.4"] [org.apache.httpcomponents/httpmime "4.5.13"] [commons-codec "1.15"] [commons-io "2.8.0"] [slingshot "0.12.2"] [potemkin "0.4.5"]] :resource-paths ["resources"] :profiles {:dev {:dependencies [;; optional deps [cheshire "5.10.0"] [crouton "0.1.2" :exclusions [[org.jsoup/jsoup]]] [org.jsoup/jsoup "1.13.1"] [org.clojure/tools.reader "1.3.5"] [com.cognitect/transit-clj "1.0.324"] [ring/ring-codec "1.1.3"] ;; other (testing) deps [org.clojure/clojure "1.10.3"] [org.clojure/tools.logging "1.1.0"] [ring/ring-jetty-adapter "1.9.3"] [ring/ring-devel "1.9.3"] ;; caching example deps [org.clojure/core.cache "1.0.207"] ;; logging [org.apache.logging.log4j/log4j-api "2.14.1"] [org.apache.logging.log4j/log4j-core "2.14.1"] [org.apache.logging.log4j/log4j-1.2-api "2.14.1"]] :plugins [[lein-ancient "0.7.0"] [jonase/eastwood "0.2.5"] [lein-kibit "0.1.5"] [lein-nvd "0.5.2"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}} :aliases {"all" ["with-profile" "dev,1.6:dev,1.7:dev,1.8:dev,1.9:dev,1.10:dev"]} :plugins [[codox "0.6.4"]] :test-selectors {:default #(not (:integration %)) :integration :integration :all (constantly true)}) clj-http-3.12.3/resources/000077500000000000000000000000001407075231000153365ustar00rootroot00000000000000clj-http-3.12.3/resources/example-log4j2.properties000066400000000000000000000030001407075231000221770ustar00rootroot00000000000000### # While no means required, this is an example log4j2.properties that you can use # for debugging clj-http (mostly the apache http client side). See the readme or # examples directory for how to use it. # Change this to "debug" to get debugging information rootLogger.level = info rootLogger.appenderRef.console.ref = console rootLogger.appenderRef.rolling.ref = fileLogger # Give directory path where log files should get stored property.basePath = ./log/ status = error # ConsoleAppender will print logs on console appender.console.type = Console appender.console.name = console appender.console.layout.type = PatternLayout # Specify the pattern of the logs appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n # RollingFileAppender will print logs in file which can be rotated based on time # or size appender.rolling.type = RollingFile appender.rolling.name = fileLogger appender.rolling.fileName=${basePath}/clj-http.log appender.rolling.filePattern=${basePath}clj-http_%d{yyyyMMdd}.log.gz appender.rolling.layout.type = PatternLayout appender.rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n appender.rolling.policies.type = Policies # Mention package name here in place of example. Classes in this package or # subpackages will use ConsoleAppender and RollingFileAppender for logging logger.example.name = example logger.example.level = debug logger.example.additivity = false logger.example.appenderRef.rolling.ref = fileLogger logger.example.appenderRef.console.ref = console clj-http-3.12.3/src/000077500000000000000000000000001407075231000141135ustar00rootroot00000000000000clj-http-3.12.3/src/clj_http/000077500000000000000000000000001407075231000157225ustar00rootroot00000000000000clj-http-3.12.3/src/clj_http/client.clj000066400000000000000000001320211407075231000176710ustar00rootroot00000000000000(ns clj-http.client "Batteries-included HTTP client." (:refer-clojure :exclude [get update]) (:require [clj-http.conn-mgr :as conn] [clj-http.cookies :refer [wrap-cookies]] [clj-http.core :as core] [clj-http.headers :refer [wrap-header-map]] [clj-http.links :refer [wrap-links]] [clj-http.util :as util :refer [opt]] [clojure.java.io :as io] [clojure.stacktrace :refer [root-cause]] [clojure.string :as str] [clojure.walk :refer [keywordize-keys prewalk]] [slingshot.slingshot :refer [throw+]]) (:import [java.io BufferedReader ByteArrayInputStream ByteArrayOutputStream EOFException File InputStream] [java.net UnknownHostException URL] [org.apache.http.entity BufferedHttpEntity ByteArrayEntity FileEntity InputStreamEntity StringEntity] org.apache.http.impl.conn.PoolingHttpClientConnectionManager org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager)) ;; Cheshire is an optional dependency, so we check for it at compile time. (def json-enabled? (try (require 'cheshire.core) true (catch Throwable _ false))) ;; Crouton is an optional dependency, so we check for it at compile time. (def crouton-enabled? (try (require 'crouton.html) true (catch Throwable _ false))) ;; tools.reader is an optional dependency, so check at compile time. (def edn-enabled? (try (require 'clojure.tools.reader.edn) true (catch Throwable _ false))) ;; Transit is an optional dependency, so check at compile time. (def transit-enabled? (try (require 'cognitect.transit) true (catch Throwable _ false))) ;; ring-codec is an optional dependency, so we check for it at compile time. (def ring-codec-enabled? (try (require 'ring.util.codec) true (catch Throwable _ false))) (defn ^:dynamic parse-edn "Resolve and apply tool.reader's EDN parsing." [& args] {:pre [edn-enabled?]} (apply (ns-resolve (symbol "clojure.tools.reader.edn") (symbol "read-string")) {:readers @(or (resolve '*data-readers*) (atom {}))} args)) (defn ^:dynamic parse-html "Resolve and apply crouton's HTML parsing." [& args] {:pre [crouton-enabled?]} (apply (ns-resolve (symbol "crouton.html") (symbol "parse")) args)) (defn- transit-opts-by-type "Return the Transit options by type." [opts type class-name] {:pre [transit-enabled?]} (cond (empty? opts) opts (contains? opts type) (clojure.core/get opts type) :else (let [class (Class/forName class-name)] (println "Deprecated use of :transit-opts found.") (update-in opts [:handlers] (fn [handlers] (->> handlers (filter #(instance? class (second %))) (into {}))))))) (defn- transit-read-opts "Return the Transit read options." [opts] {:pre [transit-enabled?]} (transit-opts-by-type opts :decode "com.cognitect.transit.ReadHandler")) (defn- transit-write-opts "Return the Transit write options." [opts] {:pre [transit-enabled?]} (transit-opts-by-type opts :encode "com.cognitect.transit.WriteHandler")) (defn ^:dynamic parse-transit "Resolve and apply Transit's JSON/MessagePack decoding." [^InputStream in type & [opts]] {:pre [transit-enabled?]} (when (pos? (.available in)) (let [reader (ns-resolve 'cognitect.transit 'reader) read (ns-resolve 'cognitect.transit 'read)] (read (reader in type (transit-read-opts opts)))))) (defn ^:dynamic transit-encode "Resolve and apply Transit's JSON/MessagePack encoding." [out type & [opts]] {:pre [transit-enabled?]} (let [output (ByteArrayOutputStream.) writer (ns-resolve 'cognitect.transit 'writer) write (ns-resolve 'cognitect.transit 'write)] (write (writer output type (transit-write-opts opts)) out) (.toByteArray output))) (defn ^:dynamic json-encode "Resolve and apply cheshire's json encoding dynamically." [& args] {:pre [json-enabled?]} (apply (ns-resolve (symbol "cheshire.core") (symbol "encode")) args)) (defn ^:dynamic json-decode "Resolve and apply cheshire's json decoding dynamically." [& args] {:pre [json-enabled?]} (if-let [json-decode-fn (ns-resolve (symbol "cheshire.core") (symbol "parse-stream-strict"))] (apply json-decode-fn args) (throw (IllegalStateException. "Missing #'cheshire.core/parse-stream-strict. Ensure the version of `cheshire` is >= 5.9.0")))) (defn ^:dynamic form-decode "Resolve and apply ring-codec's form decoding dynamically." [& args] {:pre [ring-codec-enabled?]} (apply (ns-resolve (symbol "ring.util.codec") (symbol "form-decode")) args)) (defn update [m k f & args] (assoc m k (apply f (m k) args))) (defn when-pos [v] (when (and v (pos? v)) v)) (defn dissoc-in "Dissociates an entry from a nested associative structure returning a new nested structure. keys is a sequence of keys. Any empty maps that result will not be present in the new structure." [m [k & ks :as keys]] (if ks (if-let [nextmap (clojure.core/get m k)] (let [newmap (dissoc-in nextmap ks)] (if (seq newmap) (assoc m k newmap) (dissoc m k))) m) (dissoc m k))) (defn url-encode-illegal-characters "Takes a raw url path or query and url-encodes any illegal characters. Minimizes ambiguity by encoding space to %20." [path-or-query] (when path-or-query (-> path-or-query (str/replace " " "%20") (str/replace #"[^a-zA-Z0-9\.\-\_\~\!\$\&\'\(\)\*\+\,\;\=\:\@\/\%\?]" util/url-encode)))) (defn parse-url "Parse a URL string into a map of interesting parts." [url] (let [url-parsed (URL. url)] {:scheme (keyword (.getProtocol url-parsed)) :server-name (.getHost url-parsed) :server-port (when-pos (.getPort url-parsed)) :url url :uri (url-encode-illegal-characters (.getPath url-parsed)) :user-info (if-let [user-info (.getUserInfo url-parsed)] (util/url-decode user-info)) :query-string (url-encode-illegal-characters (.getQuery url-parsed))})) (defn unparse-url "Takes a map of url-parts and generates a string representation. WARNING: does not do any sort of encoding! Don't use this for strict RFC following!" [{:keys [scheme server-name server-port uri user-info query-string]}] (str (name scheme) "://" (if (seq user-info) (str user-info "@" server-name) server-name) (when server-port (str ":" server-port)) uri (when (seq query-string) (str "?" query-string)))) ;; Statuses for which clj-http will not throw an exception (def unexceptional-status? #{200 201 202 203 204 205 206 207 300 301 302 303 304 307 308}) (defn unexceptional-status-for-request? [req status] ((or (:unexceptional-status req) unexceptional-status?) status)) ;; helper methods to determine realm of a response (defn success? [{:keys [status]}] (<= 200 status 299)) (defn missing? [{:keys [status]}] (= status 404)) (defn conflict? [{:keys [status]}] (= status 409)) (defn redirect? [{:keys [status]}] (<= 300 status 399)) (defn client-error? [{:keys [status]}] (<= 400 status 499)) (defn server-error? [{:keys [status]}] (<= 500 status 599)) (defn- exceptions-response [req {:keys [status] :as resp}] (if (unexceptional-status-for-request? req status) resp (if (false? (opt req :throw-exceptions)) resp (let [data (assoc resp :type ::unexceptional-status)] (if (opt req :throw-entire-message) (throw+ data "clj-http: status %d %s" (:status %) resp) (throw+ data "clj-http: status %s" (:status %))))))) (defn wrap-exceptions "Middleware that throws a slingshot exception if the response is not a regular response. If :throw-entire-message? is set to true, the entire response is used as the message, instead of just the status number." [client] (fn ([req] (exceptions-response req (client req))) ([req response raise] (client req (fn [resp] (response (exceptions-response req resp))) raise)))) (declare wrap-redirects) (declare reuse-pool) (defn- follow-redirect-request [req redirect trace-redirects resp] (-> req (merge (parse-url redirect)) (dissoc :query-params) (assoc :url redirect) (assoc :trace-redirects trace-redirects) (reuse-pool resp))) (defn follow-redirect "Attempts to follow the redirects from the \"location\" header, if no such header exists (bad server!), returns the response without following the request." [client {:keys [uri url scheme server-name server-port async? respond raise] :as req} {:keys [trace-redirects ^InputStream body] :as resp}] (let [url (or url (str (name scheme) "://" server-name (when server-port (str ":" server-port)) uri))] (if-let [raw-redirect (get-in resp [:headers "location"])] (let [redirect (str (URL. (URL. url) raw-redirect))] (try (.close body) (catch Exception _)) (if-not async? ((wrap-redirects client) (follow-redirect-request req redirect trace-redirects resp)) (if (some nil? [respond raise]) (raise (IllegalArgumentException. "If :async? is true, you must set :respond and :raise")) ((wrap-redirects client) (follow-redirect-request req redirect trace-redirects resp) respond raise)))) ;; Oh well, we tried, but if no location is set, return the response (if-not async? resp (respond resp))))) (defn- respond* [resp req] (if (opt req :async) ((:respond req) resp) resp)) (defn- redirects-response [client {:keys [request-method max-redirects redirects-count trace-redirects url] :or {redirects-count 1 trace-redirects [] ;; max-redirects default taken from Firefox max-redirects 20} :as req} {:keys [status] :as resp}] (let [resp-r (assoc resp :trace-redirects (if url (conj trace-redirects url) trace-redirects))] (cond (false? (opt req :follow-redirects)) (respond* resp req) (not (redirect? resp-r)) (respond* resp-r req) (and max-redirects (> redirects-count max-redirects)) (if (opt req :throw-exceptions) (throw+ resp-r "Too many redirects: %s" redirects-count) (respond* resp-r req)) (= 303 status) (follow-redirect client (assoc req :request-method :get :redirects-count (inc redirects-count)) resp-r) (#{301 302} status) (cond (#{:get :head} request-method) (follow-redirect client (assoc req :redirects-count (inc redirects-count)) resp-r) (opt req :force-redirects) (follow-redirect client (assoc req :request-method :get :redirects-count (inc redirects-count)) resp-r) :else (respond* resp-r req)) (#{307 308} status) (if (or (#{:get :head} request-method) (opt req :force-redirects)) (follow-redirect client (assoc req :redirects-count (inc redirects-count)) resp-r) (respond* resp-r req)) :else (respond* resp-r req)))) (defn ^:deprecated wrap-redirects "Middleware that follows redirects in the response. A slingshot exception is thrown if too many redirects occur. Options :follow-redirects - default:true, whether to follow redirects :max-redirects - default:20, maximum number of redirects to follow :force-redirects - default:false, force redirecting methods to GET requests In the response: :redirects-count - number of redirects :trace-redirects - vector of sites the request was redirected from" [client] (fn ([req] (redirects-response client req (client req))) ([req respond raise] (client req #(redirects-response client (assoc req :async? true :respond respond :raise raise) %) raise)))) ;; Multimethods for Content-Encoding dispatch automatically ;; decompressing response bodies (defmulti decompress-body (fn [resp] (get-in resp [:headers "content-encoding"]))) (defmethod decompress-body "gzip" [resp] (-> resp (update :body util/gunzip) (assoc :orig-content-encoding (get-in resp [:headers "content-encoding"])) (dissoc-in [:headers "content-encoding"]))) (defmethod decompress-body "deflate" [resp] (-> resp (update :body util/inflate) (assoc :orig-content-encoding (get-in resp [:headers "content-encoding"])) (dissoc-in [:headers "content-encoding"]))) (defmethod decompress-body :default [resp] (assoc resp :orig-content-encoding (get-in resp [:headers "content-encoding"]))) (defn- decompression-request [req] (if (false? (opt req :decompress-body)) req (update-in req [:headers "accept-encoding"] #(str/join ", " (remove nil? [% "gzip, deflate"]))))) (defn- decompression-response [req resp] (if (false? (opt req :decompress-body)) resp (decompress-body resp))) (defn wrap-decompression "Middleware handling automatic decompression of responses from web servers. If :decompress-body is set to false, does not automatically set `Accept-Encoding` header or decompress body." [client] (fn ([req] (decompression-response req (client (decompression-request req)))) ([req respond raise] (client (decompression-request req) #(respond (decompression-response req %)) raise)))) ;; Multimethods for coercing body type to the :as key (defmulti coerce-response-body (fn [req _] (:as req))) (defmethod coerce-response-body :byte-array [_ resp] (update resp :body util/force-byte-array)) (defmethod coerce-response-body :stream [_ resp] (update resp :body util/force-stream)) (defn- response-charset [response] (or (-> response :content-type-params :charset) "UTF-8")) (defmethod coerce-response-body :reader [_ {:keys [body] :as resp}] (let [header (get-in resp [:headers "content-type"]) parsed-values (util/parse-content-type header) charset (response-charset parsed-values)] (assoc resp :body (io/reader body :encoding charset)))) (defn- can-parse-body? [{:keys [coerce] :as request} {:keys [status] :as _response}] (or (= coerce :always) (and (unexceptional-status-for-request? request status) (or (nil? coerce) (= coerce :unexceptional))) (and (not (unexceptional-status-for-request? request status)) (= coerce :exceptional)))) (defn- decode-json-body [body keyword? charset] (let [^BufferedReader br (io/reader (util/force-stream body) :encoding charset)] (try (.mark br 1) (let [first-char (int (try (.read br) (catch EOFException _ -1)))] (case first-char -1 nil (do (.reset br) (json-decode br keyword?)))) (finally (.close br))))) (defn coerce-json-body [request {:keys [body] :as resp} keyword? & [charset]] {:pre [json-enabled?]} (let [charset (or charset (response-charset resp)) body (if (can-parse-body? request resp) (decode-json-body body keyword? charset) (util/force-string body charset))] (assoc resp :body body))) (defn coerce-clojure-body [_request {:keys [body] :as resp}] (let [charset (response-charset resp) body (util/force-string body charset)] (assoc resp :body (cond (empty? body) nil edn-enabled? (parse-edn body) :else (binding [*read-eval* false] (read-string body)))))) (defn coerce-transit-body [{:keys [transit-opts] :as request} {:keys [body] :as resp} type & [charset]] {:pre [transit-enabled?]} (let [charset (or charset (response-charset resp)) body (if (can-parse-body? request resp) (with-open [in (util/force-stream body)] (parse-transit in type transit-opts)) (util/force-string body charset))] (assoc resp :body body))) (defn coerce-form-urlencoded-body [_request {:keys [body] :as resp}] {:pre [ring-codec-enabled?]} (let [charset (response-charset resp) body (util/force-string body charset)] (assoc resp :body (-> body form-decode keywordize-keys)))) (defmulti coerce-content-type (fn [req resp] (:content-type resp))) (defmethod coerce-content-type :application/clojure [req resp] (coerce-clojure-body req resp)) (defmethod coerce-content-type :application/edn [req resp] (coerce-clojure-body req resp)) (defmethod coerce-content-type :application/json [req resp] (coerce-json-body req resp true false)) (defmethod coerce-content-type :application/transit+json [req resp] (coerce-transit-body req resp :json)) (defmethod coerce-content-type :application/transit+msgpack [req resp] (coerce-transit-body req resp :msgpack)) (defmethod coerce-content-type :application/x-www-form-urlencoded [req resp] (coerce-form-urlencoded-body req resp)) (defmethod coerce-content-type :default [req resp] (if-let [charset (-> resp :content-type-params :charset)] (coerce-response-body {:as charset} resp) (coerce-response-body {:as :default} resp))) (defmethod coerce-response-body :auto [request resp] (let [header (get-in resp [:headers "content-type"])] (->> (merge resp (util/parse-content-type header)) (coerce-content-type request)))) (defmethod coerce-response-body :json [req resp] (coerce-json-body req resp true)) (defmethod coerce-response-body :json-string-keys [req resp] (coerce-json-body req resp false)) ;; There is no longer any distinction between strict and non-strict JSON parsing ;; options. ;; ;; `:json-strict` and `:json-strict-string-keys` will be removed in a future version (defmethod coerce-response-body :json-strict [req resp] (coerce-json-body req resp true)) (defmethod coerce-response-body :json-strict-string-keys [req resp] (coerce-json-body req resp false)) (defmethod coerce-response-body :clojure [req resp] (coerce-clojure-body req resp)) (defmethod coerce-response-body :transit+json [req resp] (coerce-transit-body req resp :json)) (defmethod coerce-response-body :transit+msgpack [req resp] (coerce-transit-body req resp :msgpack)) (defmethod coerce-response-body :x-www-form-urlencoded [req resp] (coerce-form-urlencoded-body req resp)) (defmethod coerce-response-body :default [{:keys [as]} {:keys [body] :as resp}] (assoc resp :body (util/force-string body (if (string? as) as "UTF-8")))) (defn- output-coercion-response [req {:keys [body] :as resp}] (if body (coerce-response-body req resp) resp)) (defn wrap-output-coercion "Middleware converting a response body from a byte-array to a different object. Defaults to a String if no :as key is specified, the `coerce-response-body` multimethod may be extended to add additional coercions." [client] (fn ([req] (output-coercion-response req (client req))) ([req respond raise] (client req #(respond (output-coercion-response req %)) raise)))) (defn maybe-wrap-entity "Wrap an HttpEntity in a BufferedHttpEntity if warranted." [{:keys [entity-buffering]} entity] (if (and entity-buffering (not= BufferedHttpEntity (class entity))) (BufferedHttpEntity. entity) entity)) (defn- input-coercion-request [{:keys [body body-encoding length] :or {^String body-encoding "UTF-8"} :as req}] (if body (cond (string? body) (-> req (assoc :body (maybe-wrap-entity req (StringEntity. ^String body ^String body-encoding)) :character-encoding (or body-encoding "UTF-8"))) (instance? File body) (-> req (assoc :body (maybe-wrap-entity req (FileEntity. ^File body ^String body-encoding)))) ;; A length of -1 instructs HttpClient to use chunked encoding. (instance? InputStream body) (-> req (assoc :body (if length (InputStreamEntity. ^InputStream body (long length)) (maybe-wrap-entity req (InputStreamEntity. ^InputStream body -1))))) (instance? (Class/forName "[B") body) (-> req (assoc :body (maybe-wrap-entity req (ByteArrayEntity. body)))) :else req) req)) (defn wrap-input-coercion "Middleware coercing the :body of a request from a number of formats into an Apache Entity. Currently supports Strings, Files, InputStreams and byte-arrays." [client] (fn ([req] (client (input-coercion-request req))) ([req respond raise] (client (input-coercion-request req) respond raise)))) (defn get-headers-from-body "Given a map of body content, return a map of header-name to header-value." [body-map] (let [;; parse out HTML content h (or (:content body-map) (:content (first (filter #(= (:tag %) :html) body-map)))) ;; parse out tags heads (:content (first (filter #(= (:tag %) :head) h))) ;; parse out attributes of 'meta' head tags attrs (map :attrs (filter #(= (:tag %) :meta) heads)) ;; parse out the 'http-equiv' meta head tags http-attrs (filter :http-equiv attrs) ;; parse out HTML5 charset meta tags html5-charset (filter :charset attrs) ;; convert http-attributes into map of headers (lowercased) headers (apply merge (map (fn [{:keys [http-equiv content]}] {(.toLowerCase ^String http-equiv) content}) http-attrs)) ;; merge in html5 charset setting headers (merge headers (when-let [cs (:charset (first html5-charset))] {"content-type" (str "text/html; charset=" cs)}))] headers)) (defn- additional-header-parsing-response [req resp] (if (and (opt req :decode-body-headers) crouton-enabled? (:body resp) (let [^String content-type (get-in resp [:headers "content-type"])] (or (str/blank? content-type) (.startsWith content-type "text")))) (let [body-bytes (util/force-byte-array (:body resp)) body-stream1 (java.io.ByteArrayInputStream. body-bytes) body-map (parse-html body-stream1) additional-headers (get-headers-from-body body-map) body-stream2 (java.io.ByteArrayInputStream. body-bytes)] (assoc resp :headers (merge (:headers resp) additional-headers) :body body-stream2)) resp)) (defn wrap-additional-header-parsing "Middleware that parses additional http headers from the body of a web page, adding them into the headers map of the response if any are found. Only looks at the body if the :decode-body-headers option is set to a truthy value. Will be silently disabled if crouton is excluded from clj-http's dependencies. Will do nothing if no body is returned, e.g. HEAD requests" [client] (fn ([req] (additional-header-parsing-response req (client req))) ([req respond raise] (client req #(respond (additional-header-parsing-response req %)) raise)))) (defn content-type-value [type] (if (keyword? type) (str "application/" (name type)) type)) (defn- content-type-request [{:keys [content-type character-encoding] :as req}] (if content-type (let [ctv (content-type-value content-type) ct (if character-encoding (str ctv "; charset=" character-encoding) ctv)] (update-in req [:headers] assoc "content-type" ct)) req)) (defn wrap-content-type "Middleware converting a `:content-type ` option to the formal application/ format and adding it as a header." [client] (fn ([req] (client (content-type-request req))) ([req respond raise] (client (content-type-request req) respond raise)))) (defn- accept-request [{:keys [accept] :as req}] (if accept (-> req (dissoc :accept) (assoc-in [:headers "accept"] (content-type-value accept))) req)) (defn wrap-accept "Middleware converting the :accept key in a request to application/" [client] (fn ([req] (client (accept-request req))) ([req respond raise] (client (accept-request req) respond raise)))) (defn accept-encoding-value [accept-encoding] (str/join ", " (map name accept-encoding))) (defn- accept-encoding-request [{:keys [accept-encoding] :as req}] (if accept-encoding (-> req (dissoc :accept-encoding) (assoc-in [:headers "accept-encoding"] (accept-encoding-value accept-encoding))) req)) (defn wrap-accept-encoding "Middleware converting the :accept-encoding option to an acceptable Accept-Encoding header in the request." [client] (fn ([req] (client (accept-encoding-request req))) ([req respond raise] (client (accept-encoding-request req) respond raise)))) (defn detect-charset "Given a charset header, detect the charset, returns UTF-8 if not found." [content-type] (or (when-let [found (when content-type (re-find #"(?i)charset\s*=\s*([^\s]+)" content-type))] (second found)) "UTF-8")) (defn- multi-param-entries [key values multi-param-style encoding] (let [key (util/url-encode (name key) encoding) values (map #(util/url-encode (str %) encoding) values)] (case multi-param-style :indexed (map-indexed #(vector (str key \[ %1 \]) %2) values) :array (map #(vector (str key "[]") %) values) :comma-separated ;; See sub-delims in https://tools.ietf.org/html/rfc3986#section-2.2 [[key (str/join "," values)]] ;; default: repeat the key multiple times (map #(vector key %) values)))) (defn generate-query-string-with-encoding [params encoding multi-param-style] (str/join "&" (mapcat (fn [[k v]] (if (sequential? v) (map #(str/join "=" %) (multi-param-entries k v multi-param-style encoding)) [(str (util/url-encode (name k) encoding) "=" (util/url-encode (str v) encoding))])) params))) (defn generate-query-string [params & [content-type multi-param-style]] (let [encoding (detect-charset content-type)] (generate-query-string-with-encoding params encoding multi-param-style))) (defn- query-params-request [{:keys [query-params content-type multi-param-style] :or {content-type :x-www-form-urlencoded} :as req}] (if query-params (-> req (dissoc :query-params) (update-in [:query-string] (fn [old-query-string new-query-string] (if-not (empty? old-query-string) (str old-query-string "&" new-query-string) new-query-string)) (generate-query-string query-params (content-type-value content-type) multi-param-style))) req)) (defn wrap-query-params "Middleware converting the :query-params option to a querystring on the request." [client] (fn ([req] (client (query-params-request req))) ([req respond raise] (client (query-params-request req) respond raise)))) (defn basic-auth-value [basic-auth] (let [basic-auth (if (string? basic-auth) basic-auth (str (first basic-auth) ":" (second basic-auth)))] (str "Basic " (util/base64-encode (util/utf8-bytes basic-auth))))) (defn- basic-auth-request [req] (if-let [basic-auth (:basic-auth req)] (-> req (dissoc :basic-auth) (assoc-in [:headers "authorization"] (basic-auth-value basic-auth))) req)) (defn wrap-basic-auth "Middleware converting the :basic-auth option into an Authorization header." [client] (fn ([req] (client (basic-auth-request req))) ([req respond raise] (client (basic-auth-request req) respond raise)))) (defn- oauth-request [req] (if-let [oauth-token (:oauth-token req)] (-> req (dissoc :oauth-token) (assoc-in [:headers "authorization"] (str "Bearer " oauth-token))) req)) (defn wrap-oauth "Middleware converting the :oauth-token option into an Authorization header." [client] (fn ([req] (client (oauth-request req))) ([req respond raise] (client (oauth-request req) respond raise)))) (defn parse-user-info [user-info] (when user-info (str/split user-info #":"))) (defn- user-info-request [req] (if-let [[user password] (parse-user-info (:user-info req))] (assoc req :basic-auth [user password]) req)) (defn wrap-user-info "Middleware converting the :user-info option into a :basic-auth option" [client] (fn ([req] (client (user-info-request req))) ([req respond raise] (client (user-info-request req) respond raise)))) (defn- method-request [req] (if-let [m (:method req)] (-> req (dissoc :method) (assoc :request-method m)) req)) (defn wrap-method "Middleware converting the :method option into the :request-method option" [client] (fn ([req] (client (method-request req))) ([req respond raise] (client (method-request req) respond raise)))) (defmulti coerce-form-params (fn [req] (keyword (content-type-value (:content-type req))))) (defmethod coerce-form-params :application/edn [{:keys [form-params]}] (pr-str form-params)) (defn- coerce-transit-form-params [type {:keys [form-params transit-opts]}] (when-not transit-enabled? (throw (ex-info (format (str "Can't encode form params as " "\"application/transit+%s\". " "Transit dependency not loaded.") (name type)) {:type :transit-not-loaded :form-params form-params :transit-opts transit-opts :transit-type type}))) (transit-encode form-params type transit-opts)) (defmethod coerce-form-params :application/transit+json [req] (coerce-transit-form-params :json req)) (defmethod coerce-form-params :application/transit+msgpack [req] (coerce-transit-form-params :msgpack req)) (defmethod coerce-form-params :application/json [{:keys [form-params json-opts]}] (when-not json-enabled? (throw (ex-info (str "Can't encode form params as \"application/json\". " "Cheshire dependency not loaded.") {:type :cheshire-not-loaded :form-params form-params :json-opts json-opts}))) (json-encode form-params json-opts)) (defmethod coerce-form-params :default [{:keys [content-type multi-param-style form-params form-param-encoding]}] (if form-param-encoding (generate-query-string-with-encoding form-params form-param-encoding multi-param-style) (generate-query-string form-params (content-type-value content-type) multi-param-style))) (defn- form-params-request [{:keys [form-params content-type request-method] :or {content-type :x-www-form-urlencoded} :as req}] (if (and form-params (#{:post :put :patch :delete} request-method)) (-> req (dissoc :form-params) (assoc :content-type (content-type-value content-type) :body (coerce-form-params req))) req)) (defn wrap-form-params "Middleware wrapping the submission or form parameters." [client] (fn ([req] (client (form-params-request req))) ([req respnd raise] (client (form-params-request req) respnd raise)))) (defn- nest-params [request param-key] (if-let [params (request param-key)] (assoc request param-key (prewalk #(if (and (vector? %) (map? (second %))) (let [[fk m] %] (reduce (fn [m [sk v]] (assoc m (str (name fk) \[ (name sk) \]) v)) {} m)) %) params)) request)) (defn- nest-params-request [{:keys [flatten-nested-keys] :as req}] (if (seq flatten-nested-keys) (reduce nest-params req flatten-nested-keys) req)) (defn wrap-nested-params "Middleware wrapping nested parameters for query strings." [client] (fn ([req] (client (nest-params-request req))) ([req respond raise] (client (nest-params-request req) respond raise)))) (defn- nested-keys-to-flatten [{:keys [flatten-nested-keys] :as req}] (when (and (or (not (nil? (opt req :ignore-nested-query-string))) (not (nil? (opt req :flatten-nested-form-params)))) flatten-nested-keys) (throw (IllegalArgumentException. (str "only :flatten-nested-keys or :ignore-nested-query-string/" ":flatten-nested-keys may be specified, not both")))) (let [iqs-key (when-not (opt req :ignore-nested-query-string) :query-params) ifp-key (when (opt req :flatten-nested-form-params) :form-params)] (or flatten-nested-keys (remove nil? (list iqs-key ifp-key))))) (defn wrap-flatten-nested-params "Middleware wrapping options for whether or not to flatten `:query-params` and `:form-params`. Modifies the request by adding a `:flatten-nested-keys` sequence of the nested keys that will be flattened." [client] (fn ([req] (client (assoc req :flatten-nested-keys (nested-keys-to-flatten req)))) ([req respond raise] (client (assoc req :flatten-nested-keys (nested-keys-to-flatten req)) respond raise)))) (defn- url-request [req] (if-let [url (:url req)] (-> req (dissoc :url) (merge (parse-url url))) req)) (defn wrap-url "Middleware wrapping request URL parsing." [client] (fn ([req] (client (url-request req))) ([req respond raise] (client (url-request req) respond raise)))) (defn wrap-unknown-host "Middleware ignoring unknown hosts when the :ignore-unknown-host? option is set." [client] (fn ([req] (try (client req) (catch Exception e (if (= (type (root-cause e)) UnknownHostException) (when-not (opt req :ignore-unknown-host) (throw (root-cause e))) (throw (root-cause e)))))) ([req respond raise] (client (assoc req :unknown-host-respond respond) respond raise)))) (defn wrap-lower-case-headers "Middleware lowercasing all headers, as per RFC (case-insensitive) and Ring spec." [client] (let [lower-case-headers #(if-let [headers (:headers %1)] (assoc %1 :headers (util/lower-case-keys headers)) %1)] (fn ([req] (-> (client (lower-case-headers req)) (lower-case-headers))) ([req respond raise] (client (lower-case-headers req) #(respond (lower-case-headers %)) raise))))) (defn- request-timing-response [resp start] (assoc resp :request-time (- (System/currentTimeMillis) start))) (defn wrap-request-timing "Middleware that times the request, putting the total time (in milliseconds) of the request into the :request-time key in the response." [client] (fn ([req] (let [start (System/currentTimeMillis) resp (client req)] (request-timing-response resp start))) ([req respond raise] (let [start (System/currentTimeMillis)] (client req #(respond (request-timing-response % start)) raise))))) (def default-middleware "The default list of middleware clj-http uses for wrapping requests." [wrap-request-timing wrap-header-map wrap-query-params wrap-basic-auth wrap-oauth wrap-user-info wrap-url wrap-decompression wrap-input-coercion ;; put this before output-coercion, so additional charset ;; headers can be used if desired wrap-additional-header-parsing wrap-output-coercion wrap-exceptions wrap-accept wrap-accept-encoding wrap-content-type wrap-form-params wrap-nested-params wrap-flatten-nested-params wrap-method wrap-cookies wrap-links wrap-unknown-host]) (def ^:dynamic *current-middleware* "Available at any time to retrieve the middleware being used. Automatically bound when `with-middleware` is used." default-middleware) (defn wrap-request "Returns a batteries-included HTTP request function corresponding to the given core client. See default-middleware for the middleware wrappers that are used by default" [request] (reduce (fn wrap-request* [request middleware] (middleware request)) request default-middleware)) (def ^:dynamic request "Executes the HTTP request corresponding to the given map and returns the response map for corresponding to the resulting HTTP response. In addition to the standard Ring request keys, the following keys are also recognized: * :url * :method * :query-params * :basic-auth * :content-type * :accept * :accept-encoding * :as The following keys make an async HTTP request, like ring's CPS handler. * :async? * :respond * :raise The following additional behaviors are also automatically enabled: * Exceptions are thrown for status codes other than 200-207, 300-303, or 307 * Gzip and deflate responses are accepted and decompressed * Input and output bodies are coerced as required and indicated by the :as option." (wrap-request #'core/request)) ;; Inline function to throw a slightly more readable exception when ;; the URL is nil (definline check-url! [url] `(when (nil? ~url) (throw (IllegalArgumentException. "Host URL cannot be nil")))) (defn- request* [req [respond raise]] (if (opt req :async) (if (some nil? [respond raise]) (throw (IllegalArgumentException. "If :async? is true, you must pass respond and raise")) (request (dissoc req :respond :raise) respond raise)) (request req))) (defn get "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :get :url url}) r)) (defn head "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :head :url url}) r)) (defn post "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :post :url url}) r)) (defn put "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :put :url url}) r)) (defn delete "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :delete :url url}) r)) (defn options "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :options :url url}) r)) (defn copy "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :copy :url url}) r)) (defn move "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :move :url url}) r)) (defn patch "Like #'request, but sets the :method and :url as appropriate." [url & [req & r]] (check-url! url) (request* (merge req {:method :patch :url url}) r)) (defmacro with-middleware "Perform the body of the macro with a custom middleware list. It is highly recommended to at least include: clj-http.client/wrap-url clj-http.client/wrap-method Unless you really know what you are doing." [middleware & body] `(let [m# ~middleware] (binding [*current-middleware* m# clj-http.client/request (reduce #(%2 %1) clj-http.core/request m#)] ~@body))) (defmacro with-additional-middleware "Perform the body of the macro with a list of additional middleware. The given `middleware-seq' is concatenated to the beginning of the `*current-middleware*' sequence." [middleware-seq & body] `(with-middleware (concat ~middleware-seq *current-middleware*) ~@body)) (defmacro with-connection-pool "Macro to execute the body using a connection manager. Creates a PoolingHttpClientConnectionManager to use for all requests within the body of the expression. An option map is allowed to set options for the connection manager. The following options are supported: :timeout - Time that connections are left open before automatically closing default: 5 :threads - Maximum number of threads that will be used for connecting default: 4 :default-per-route - Maximum number of simultaneous connections per host default: 2 :insecure? - Boolean flag to specify allowing insecure HTTPS connections default: false :keystore - keystore file path or KeyStore instance to be used for connection manager :keystore-pass - keystore password :trust-store - trust store file path or KeyStore instance to be used for connection manager :trust-store-pass - trust store password Note that :insecure? and :keystore/:trust-store options are mutually exclusive If the value 'nil' is specified or the value is not set, the default value will be used." [opts & body] ;; I'm leaving the connection bindable for now because in the ;; future I'm toying with the idea of managing the connection ;; manager yourself and passing it into the request `(let [cm# (conn/make-reusable-conn-manager ~opts)] (binding [conn/*connection-manager* cm#] (try ~@body (finally (.shutdown ^PoolingHttpClientConnectionManager conn/*connection-manager*)))))) (defn reuse-pool "A helper function takes a request options map and a response map respond from a pooled async request, the returned options map will be set to reuse the connection pool which used by the former request" [options response] (if-let [info (:pooling-info response)] (assoc options :pooling-info info) options)) (defmacro with-async-connection-pool "Macro to execute the body using a connection manager. Creates a PoolingNHttpClientConnectionManager to use for all requests within the body of the expression. An option map is allowed to set options for the connection manager. Handles the same options as `with-connection-pool` plus: :io-config which should be a map containing some of the following keys: :connect-timeout - int the default connect timeout value for connection requests (default 0, meaning no timeout) :interest-op-queued - boolean, whether or not I/O interest operations are to be queued and executed asynchronously or to be applied to the underlying SelectionKey immediately (default false) :io-thread-count - int, the number of I/O dispatch threads to be used (default is the number of available processors) :rcv-buf-size - int the default value of the SO_RCVBUF parameter for newly created sockets (default is 0, meaning the system default) :select-interval - long, time interval in milliseconds at which to check for timed out sessions and session requests (default 1000) :shutdown-grace-period - long, grace period in milliseconds to wait for individual worker threads to terminate cleanly (default 500) :snd-buf-size - int, the default value of the SO_SNDBUF parameter for newly created sockets (default is 0, meaning the system default) :so-keep-alive - boolean, the default value of the SO_KEEPALIVE parameter for newly created sockets (default false) :so-linger - int, the default value of the SO_LINGER parameter for newly created sockets (default -1) :so-timeout - int, the default socket timeout value for I/O operations (default 0, meaning no timeout) :tcp-no-delay - boolean, the default value of the TCP_NODELAY parameter for newly created sockets (default true) If the value 'nil' is specified or the value is not set, the default value will be used." [opts & body] `(let [cm# (conn/make-reuseable-async-conn-manager ~opts)] (binding [conn/*async-connection-manager* cm#] (try ~@body (finally (.shutdown ^PoolingNHttpClientConnectionManager cm#)))))) clj-http-3.12.3/src/clj_http/conn_mgr.clj000066400000000000000000000403261407075231000202230ustar00rootroot00000000000000(ns clj-http.conn-mgr "Utility methods for Scheme registries and HTTP connection managers" (:require [clj-http.util :refer [opt]] [clojure.java.io :as io]) (:import [java.net InetSocketAddress Proxy Proxy$Type Socket] java.security.KeyStore [javax.net.ssl HostnameVerifier KeyManager SSLContext TrustManager] [org.apache.http.config ConnectionConfig Registry RegistryBuilder SocketConfig] org.apache.http.conn.HttpClientConnectionManager org.apache.http.conn.socket.PlainConnectionSocketFactory [org.apache.http.conn.ssl DefaultHostnameVerifier NoopHostnameVerifier SSLConnectionSocketFactory SSLContexts TrustStrategy] [org.apache.http.impl.conn BasicHttpClientConnectionManager PoolingHttpClientConnectionManager] org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager org.apache.http.impl.nio.DefaultHttpClientIODispatch [org.apache.http.impl.nio.reactor DefaultConnectingIOReactor IOReactorConfig] [org.apache.http.nio.conn NHttpClientConnectionManager NoopIOSessionStrategy] org.apache.http.nio.conn.ssl.SSLIOSessionStrategy org.apache.http.nio.protocol.HttpAsyncRequestExecutor)) ;; -- Interop Helpers --------------------------------------------------------- (defn ^Registry into-registry [registry] (cond (instance? Registry registry) registry (map? registry) (let [registry-builder (RegistryBuilder/create)] (doseq [[k v] registry] (.register registry-builder k v)) (.build registry-builder)) :else (throw (IllegalArgumentException. "Cannot coerce into a Registry")))) ;; -- SocketFactory ----------------------------------------------------------- (defn ^SSLConnectionSocketFactory SSLGenericSocketFactory "Given a function that returns a new socket, create an SSLConnectionSocketFactory that will use that socket." ([socket-factory] (SSLGenericSocketFactory socket-factory nil)) ([socket-factory ^SSLContext ssl-context] (let [^SSLContext ssl-context' (or ssl-context (SSLContexts/createDefault))] (proxy [SSLConnectionSocketFactory] [ssl-context'] (connectSocket [timeout socket host remoteAddress localAddress context] (let [^SSLConnectionSocketFactory this this] ;; avoid reflection (proxy-super connectSocket timeout (socket-factory) host remoteAddress localAddress context))))))) (defn ^PlainConnectionSocketFactory PlainGenericSocketFactory "Given a Function that returns a new socket, create a PlainConnectionSocketFactory that will use that socket." [socket-factory] (proxy [PlainConnectionSocketFactory] [] (createSocket [context] (socket-factory)))) (defn socks-proxied-socket "Create a Socket proxied through socks, using the given hostname and port" [^String hostname ^Integer port] (Socket. (Proxy. Proxy$Type/SOCKS (InetSocketAddress. hostname port)))) ;; -- SSL Contexts ------------------------------------------------------------ (defn ^KeyStore get-keystore* [keystore-file keystore-type ^String keystore-pass] (when keystore-file (let [keystore (KeyStore/getInstance (or keystore-type (KeyStore/getDefaultType)))] (with-open [is (io/input-stream keystore-file)] (.load keystore is (when keystore-pass (.toCharArray keystore-pass))) keystore)))) (defn ^KeyStore get-keystore [keystore & args] (if (instance? KeyStore keystore) keystore (apply get-keystore* keystore args))) (defn- ssl-context-for-keystore ;; TODO: use something else for passwords ;; Note: JVM strings aren't ideal for passwords - see ;; https://tinyurl.com/azm3ab9 [{:keys [keystore keystore-type ^String keystore-pass trust-store trust-store-type trust-store-pass]}] (let [ks (get-keystore keystore keystore-type keystore-pass) ts (get-keystore trust-store trust-store-type trust-store-pass)] (-> (SSLContexts/custom) (.loadKeyMaterial ks (when keystore-pass (.toCharArray keystore-pass))) (.loadTrustMaterial ts nil) (.build)))) (defn- ssl-context-for-trust-or-key-manager "Given an instance or seqable data structure of TrustManager or KeyManager will create and return an SSLContexts object including the resulting managers" [{:keys [trust-managers key-managers]}] (let [x-or-xs->x-array (fn [type x-or-xs] (cond (or (-> x-or-xs class .isArray) (sequential? x-or-xs)) (into-array type (seq x-or-xs)) :else (into-array type [x-or-xs]))) trust-managers (when trust-managers (x-or-xs->x-array TrustManager trust-managers)) key-managers (when key-managers (x-or-xs->x-array KeyManager key-managers))] (doto (.build (SSLContexts/custom)) (.init key-managers trust-managers nil)))) (defn- ssl-context-insecure "Creates a SSL Context that trusts all material." [] (-> (SSLContexts/custom) (.loadTrustMaterial nil (reify TrustStrategy (isTrusted [_ chain auth-type] true))) (.build))) (defn ^SSLContext get-ssl-context "Gets the SSL Context from a request or connection pool settings" [{:keys [keystore trust-store key-managers trust-managers] :as config}] (cond (or keystore trust-store) (ssl-context-for-keystore config) (or key-managers trust-managers) (ssl-context-for-trust-or-key-manager config) (opt config :insecure) (ssl-context-insecure) :else (SSLContexts/createDefault))) (defn ^HostnameVerifier get-hostname-verifier [config] (if (opt config :insecure) NoopHostnameVerifier/INSTANCE (DefaultHostnameVerifier.))) ;; -- Connection Managers ----------------------------------------------------- (defn make-socks-proxied-conn-manager "Given an optional hostname and a port, create a connection manager that's proxied using a SOCKS proxy." ([^String hostname ^Integer port] (make-socks-proxied-conn-manager hostname port {})) ([^String hostname ^Integer port {:keys [keystore keystore-type keystore-pass trust-store trust-store-type trust-store-pass trust-managers key-managers] :as config}] (let [socket-factory #(socks-proxied-socket hostname port) registry (into-registry {"http" (PlainGenericSocketFactory socket-factory) "https" (SSLGenericSocketFactory socket-factory (get-ssl-context config))})] (PoolingHttpClientConnectionManager. registry)))) (defn ^BasicHttpClientConnectionManager make-regular-conn-manager [{:keys [dns-resolver keystore trust-store key-managers trust-managers socket-timeout] :as config}] (let [registry (into-registry {"http" (PlainConnectionSocketFactory/getSocketFactory) "https" (SSLConnectionSocketFactory. (get-ssl-context config) (get-hostname-verifier config))}) conn-manager (BasicHttpClientConnectionManager. registry nil nil dns-resolver)] (when socket-timeout (.setSocketConfig conn-manager (-> (.getSocketConfig conn-manager) (SocketConfig/copy) (.setSoTimeout socket-timeout) ;modify only the socket-timeout (.build)))) conn-manager)) (defn- ^DefaultConnectingIOReactor make-ioreactor [{:keys [connect-timeout interest-op-queued io-thread-count rcv-buf-size select-interval shutdown-grace-period snd-buf-size so-keep-alive so-linger so-timeout tcp-no-delay]}] (as-> (IOReactorConfig/custom) c (if-some [v connect-timeout] (.setConnectTimeout c v) c) (if-some [v interest-op-queued] (.setInterestOpQueued c v) c) (if-some [v io-thread-count] (.setIoThreadCount c v) c) (if-some [v rcv-buf-size] (.setRcvBufSize c v) c) (if-some [v select-interval] (.setSelectInterval c v) c) (if-some [v shutdown-grace-period] (.setShutdownGracePeriod c v) c) (if-some [v snd-buf-size] (.setSndBufSize c v) c) (if-some [v so-keep-alive] (.setSoKeepAlive c v) c) (if-some [v so-linger] (.setSoLinger c v) c) (if-some [v so-timeout] (.setSoTimeout c v) c) (if-some [v tcp-no-delay] (.setTcpNoDelay c v) c) (DefaultConnectingIOReactor. (.build c)))) (defn ^PoolingNHttpClientConnectionManager make-regular-async-conn-manager [{:keys [keystore trust-store key-managers trust-managers] :as config}] (let [^Registry registry (into-registry {"http" (NoopIOSessionStrategy/INSTANCE) "https" (SSLIOSessionStrategy. (get-ssl-context config) (get-hostname-verifier config))}) io-reactor (make-ioreactor {:shutdown-grace-period 1})] (doto (PoolingNHttpClientConnectionManager. io-reactor registry) (.setMaxTotal 1)))) (definterface ReuseableAsyncConnectionManager) ;; need the fully qualified class name because this fn is later used in a ;; macro from a different ns (defn ^org.apache.http.impl.conn.PoolingHttpClientConnectionManager make-reusable-conn-manager* "Given an timeout and optional insecure? flag, create a PoolingHttpClientConnectionManager with seconds set as the timeout value." [{:keys [dns-resolver timeout keystore trust-store key-managers trust-managers] :as config}] (let [registry (into-registry {"http" (PlainConnectionSocketFactory/getSocketFactory) "https" (SSLConnectionSocketFactory. (get-ssl-context config) (get-hostname-verifier config))})] (PoolingHttpClientConnectionManager. registry nil nil dns-resolver timeout java.util.concurrent.TimeUnit/SECONDS))) (defn reusable? [conn-mgr] (or (instance? PoolingHttpClientConnectionManager conn-mgr) (instance? ReuseableAsyncConnectionManager conn-mgr))) (defn ^PoolingHttpClientConnectionManager make-reusable-conn-manager "Creates a default pooling connection manager with the specified options. The following options are supported: :timeout - Time that connections are left open before automatically closing default: 5 :threads - Maximum number of threads that will be used for connecting default: 4 :default-per-route - Maximum number of simultaneous connections per host default: 2 :insecure? - Boolean flag to specify allowing insecure HTTPS connections default: false :keystore - keystore file to be used for connection manager :keystore-pass - keystore password :trust-store - trust store file to be used for connection manager :trust-store-pass - trust store password :key-managers - KeyManager objects to be used for connection manager :trust-managers - TrustManager objects to be used for connection manager :dns-resolver - Use a custom DNS resolver instead of the default DNS resolver. Note that :insecure? and :keystore/:trust-store/:key-managers/:trust-managers options are mutually exclusive Note that :key-managers/:trust-managers have precedence over :keystore/:trust-store options If the value 'nil' is specified or the value is not set, the default value will be used." [opts] (let [timeout (or (:timeout opts) 5) threads (or (:threads opts) 4) default-per-route (:default-per-route opts) insecure? (opt opts :insecure) leftovers (dissoc opts :timeout :threads :insecure? :insecure) conn-man (make-reusable-conn-manager* (merge {:timeout timeout :insecure? insecure?} leftovers))] (.setMaxTotal conn-man threads) (when default-per-route (.setDefaultMaxPerRoute conn-man default-per-route)) conn-man)) (defn- ^PoolingNHttpClientConnectionManager make-reusable-async-conn-manager* [{:keys [dns-resolver timeout keystore trust-store io-config key-managers trust-managers] :as config}] (let [registry (into-registry {"http" (NoopIOSessionStrategy/INSTANCE) "https" (SSLIOSessionStrategy. (get-ssl-context config) (get-hostname-verifier config))}) io-reactor (make-ioreactor io-config) protocol-handler (HttpAsyncRequestExecutor.) io-event-dispatch (DefaultHttpClientIODispatch. protocol-handler ConnectionConfig/DEFAULT)] (future (.execute io-reactor io-event-dispatch)) (proxy [PoolingNHttpClientConnectionManager ReuseableAsyncConnectionManager] [io-reactor nil registry nil dns-resolver timeout java.util.concurrent.TimeUnit/SECONDS]))) (defn ^PoolingNHttpClientConnectionManager make-reusable-async-conn-manager "Creates a default pooling async connection manager with the specified options. Handles the same options as make-reusable-conn-manager plus :io-config which should be a map containing some of the following keys: :connect-timeout - int the default connect timeout value for connection requests (default 0, meaning no timeout) :interest-op-queued - boolean, whether or not I/O interest operations are to be queued and executed asynchronously or to be applied to the underlying SelectionKey immediately (default false) :io-thread-count - int, the number of I/O dispatch threads to be used (default is the number of available processors) :rcv-buf-size - int the default value of the SO_RCVBUF parameter for newly created sockets (default is 0, meaning the system default) :select-interval - long, time interval in milliseconds at which to check for timed out sessions and session requests (default 1000) :shutdown-grace-period - long, grace period in milliseconds to wait for individual worker threads to terminate cleanly (default 500) :snd-buf-size - int, the default value of the SO_SNDBUF parameter for newly created sockets (default is 0, meaning the system default) :so-keep-alive - boolean, the default value of the SO_KEEPALIVE parameter for newly created sockets (default false) :so-linger - int, the default value of the SO_LINGER parameter for newly created sockets (default -1) :so-timeout - int, the default socket timeout value for I/O operations (default 0, meaning no timeout) :tcp-no-delay - boolean, the default value of the TCP_NODELAY parameter for newly created sockets (default true) If the value 'nil' is specified or the value is not set, the default value will be used." [opts] (let [timeout (or (:timeout opts) 5) threads (or (:threads opts) 4) default-per-route (:default-per-route opts) insecure? (opt opts :insecure) leftovers (dissoc opts :timeout :threads :insecure? :insecure) conn-man (make-reusable-async-conn-manager* (merge {:timeout timeout :insecure? insecure?} leftovers))] (.setMaxTotal conn-man threads) (when default-per-route (.setDefaultMaxPerRoute conn-man default-per-route)) conn-man)) (defn ^PoolingNHttpClientConnectionManager make-reuseable-async-conn-manager "Wraps correctly-spelled version - keeping for backwards compatibility." [opts] (make-reusable-async-conn-manager opts)) (defmulti shutdown-manager "Shut down the given connection manager, if it is not nil" class) (defmethod shutdown-manager nil [conn-mgr] nil) (defmethod shutdown-manager org.apache.http.conn.HttpClientConnectionManager [^HttpClientConnectionManager conn-mgr] (.shutdown conn-mgr)) (defmethod shutdown-manager org.apache.http.nio.conn.NHttpClientConnectionManager [^NHttpClientConnectionManager conn-mgr] (.shutdown conn-mgr)) (def ^:dynamic *connection-manager* "connection manager to be rebound during request execution" nil) (def ^:dynamic *async-connection-manager* "connection manager to be rebound during async request execution" nil) clj-http-3.12.3/src/clj_http/cookies.clj000066400000000000000000000125131407075231000200520ustar00rootroot00000000000000(ns clj-http.cookies "Namespace dealing with HTTP cookies" (:require [clj-http.util :refer [opt]] [clojure.string :refer [blank? join lower-case]]) (:import org.apache.http.client.CookieStore [org.apache.http.cookie ClientCookie CookieOrigin CookieSpec] org.apache.http.Header org.apache.http.impl.client.BasicCookieStore [org.apache.http.impl.cookie BasicClientCookie2 BrowserCompatSpecFactory] org.apache.http.message.BasicHeader org.apache.http.protocol.BasicHttpContext)) (defn cookie-spec ^org.apache.http.cookie.CookieSpec [] (.create (BrowserCompatSpecFactory.) (BasicHttpContext.))) (defn compact-map "Removes all map entries where value is nil." [m] (reduce (fn [newm k] (if (not (nil? (get m k))) (assoc newm k (get m k)) newm)) (sorted-map) (sort (keys m)))) (defn to-cookie "Converts a ClientCookie object into a tuple where the first item is the name of the cookie and the second item the content of the cookie." [^ClientCookie cookie] [(.getName cookie) (compact-map {:comment (.getComment cookie) :comment-url (.getCommentURL cookie) :discard (not (.isPersistent cookie)) :domain (.getDomain cookie) :expires (when (.getExpiryDate cookie) (.getExpiryDate cookie)) :path (.getPath cookie) :ports (when (.getPorts cookie) (seq (.getPorts cookie))) :secure (.isSecure cookie) :value (.getValue cookie) :version (.getVersion cookie)})]) (defn ^BasicClientCookie2 to-basic-client-cookie "Converts a cookie seq into a BasicClientCookie2." [[cookie-name cookie-content]] (doto (BasicClientCookie2. (name cookie-name) (name (:value cookie-content))) (.setComment (:comment cookie-content)) (.setCommentURL (:comment-url cookie-content)) (.setDiscard (:discard cookie-content true)) (.setDomain (:domain cookie-content)) (.setExpiryDate (:expires cookie-content)) (.setPath (:path cookie-content)) (.setPorts (int-array (:ports cookie-content))) (.setSecure (:secure cookie-content false)) (.setVersion (:version cookie-content 0)))) (defn decode-cookie "Decode the Set-Cookie string into a cookie seq." [set-cookie-str] (if-not (blank? set-cookie-str) ;; I just want to parse a cookie without providing origin. How? (let [domain (lower-case (str (gensym))) origin (CookieOrigin. domain 80 "/" false) [cookie-name cookie-content] (-> (cookie-spec) (.parse (BasicHeader. "set-cookie" set-cookie-str) origin) first to-cookie)] [cookie-name (if (= domain (:domain cookie-content)) (dissoc cookie-content :domain) cookie-content)]))) (defn decode-cookies "Converts a cookie string or seq of strings into a cookie map." [cookies] (reduce #(assoc %1 (first %2) (second %2)) {} (map decode-cookie (if (sequential? cookies) cookies [cookies])))) (defn decode-cookie-header "Decode the Set-Cookie header into the cookies key." [response] (if-let [cookies (get (:headers response) "set-cookie")] (assoc response :cookies (decode-cookies cookies) :headers (dissoc (:headers response) "set-cookie")) response)) (defn encode-cookie "Encode the cookie into a string used by the Cookie header." [cookie] (when-let [header (-> (cookie-spec) (.formatCookies [(to-basic-client-cookie cookie)]) first)] (.getValue ^Header header))) (defn encode-cookies "Encode the cookie map into a string." [cookie-map] (join ";" (map encode-cookie (seq cookie-map)))) (defn encode-cookie-header "Encode the :cookies key of the request into a Cookie header." [request] (if (:cookies request) (-> request (assoc-in [:headers "Cookie"] (encode-cookies (:cookies request))) (dissoc :cookies)) request)) (defn- cookies-response [request response] (if (= false (opt request :decode-cookies)) response (decode-cookie-header response))) (defn wrap-cookies "Middleware wrapping cookie handling. Handles converting the :cookies request parameter into the 'Cookies' header for an HTTP request." [client] (fn ([request] (cookies-response request (client (encode-cookie-header request)))) ([request respond raise] (client (encode-cookie-header request) #(respond (cookies-response request %)) raise)))) (defn cookie-store "Returns a new, empty instance of the default implementation of the org.apache.http.client.CookieStore interface." [] (BasicCookieStore.)) (defn get-cookies "Given a cookie-store, return a map of cookie name to a map of cookie values." [^CookieStore cookie-store] (when cookie-store (into {} (map to-cookie (.getCookies cookie-store))))) (defn add-cookie "Add a ClientCookie to a cookie-store" [^CookieStore cookie-store ^ClientCookie cookie] (.addCookie cookie-store cookie)) (defn clear-cookies "Clears all cookies from cookie-store" [^CookieStore cookie-store] (.clear cookie-store)) clj-http-3.12.3/src/clj_http/core.clj000066400000000000000000000722741407075231000173600ustar00rootroot00000000000000(ns clj-http.core "Core HTTP request/response implementation. Rewrite for Apache 4.3" (:require [clj-http.conn-mgr :as conn] [clj-http.headers :as headers] [clj-http.multipart :as mp] [clj-http.util :refer [opt]] clojure.pprint) (:import [java.io ByteArrayOutputStream FilterInputStream InputStream] [java.net InetAddress ProxySelector URI URL] java.util.Locale [org.apache.http HeaderIterator HttpEntity HttpEntityEnclosingRequest HttpHost HttpRequestInterceptor HttpResponse HttpResponseInterceptor ProtocolException] [org.apache.http.auth AuthScope NTCredentials UsernamePasswordCredentials] [org.apache.http.client CredentialsProvider HttpRequestRetryHandler RedirectStrategy] org.apache.http.client.cache.HttpCacheContext [org.apache.http.client.config CookieSpecs RequestConfig] [org.apache.http.client.methods CloseableHttpResponse HttpDelete HttpEntityEnclosingRequestBase HttpGet HttpHead HttpOptions HttpPatch HttpPost HttpPut HttpRequestBase HttpUriRequest] org.apache.http.client.protocol.HttpClientContext org.apache.http.client.utils.URIUtils org.apache.http.config.RegistryBuilder org.apache.http.conn.routing.HttpRoutePlanner org.apache.http.cookie.CookieSpecProvider [org.apache.http.entity ByteArrayEntity StringEntity] [org.apache.http.impl.client BasicCredentialsProvider CloseableHttpClient DefaultRedirectStrategy HttpClientBuilder HttpClients LaxRedirectStrategy] [org.apache.http.impl.client.cache CacheConfig CachingHttpClientBuilder] [org.apache.http.impl.conn DefaultProxyRoutePlanner SystemDefaultRoutePlanner] [org.apache.http.impl.nio.client CloseableHttpAsyncClient HttpAsyncClientBuilder HttpAsyncClients])) (def CUSTOM_COOKIE_POLICY "_custom") (defn parse-headers "Takes a HeaderIterator and returns a map of names to values. If a name appears more than once (like `set-cookie`) then the value will be a vector containing the values in the order they appeared in the headers." [^HeaderIterator headers & [use-header-maps-in-response?]] (if-not use-header-maps-in-response? (->> (headers/header-iterator-seq headers) (map (fn [[k v]] [(.toLowerCase ^String k) v])) (reduce (fn [hs [k v]] (headers/assoc-join hs k v)) {})) (->> (headers/header-iterator-seq headers) (reduce (fn [hs [k v]] (headers/assoc-join hs k v)) (headers/header-map))))) (defn graceful-redirect-strategy "Similar to the default redirect strategy, however, does not throw an error when the maximum number of redirects has been reached. Still supports validating that the new redirect host is not empty." [req] (let [validate? (opt req :validate-redirects)] (reify RedirectStrategy (getRedirect [this request response context] (let [new-request (.getRedirect DefaultRedirectStrategy/INSTANCE request response context)] (when (or validate? (nil? validate?)) (let [uri (.getURI new-request) new-host (URIUtils/extractHost uri)] (when (nil? new-host) (throw (ProtocolException. (str "Redirect URI does not specify a valid host name: " uri)))))) new-request)) (isRedirected [this request response context] (let [^HttpClientContext typed-context context max-redirects (-> (.getRequestConfig typed-context) .getMaxRedirects) num-redirects (count (.getRedirectLocations typed-context))] (if (<= max-redirects num-redirects) false (.isRedirected DefaultRedirectStrategy/INSTANCE request response typed-context))))))) (defn default-redirect-strategy "Proxies calls to whatever original redirect strategy is passed in, however, if :validate-redirects is set in the request, checks that the redirected host is not empty." [^RedirectStrategy original req] (let [validate? (opt req :validate-redirects)] (reify RedirectStrategy (getRedirect [this request response context] (let [new-request (.getRedirect original request response context)] (when (or validate? (nil? validate?)) (let [uri (.getURI new-request) new-host (URIUtils/extractHost uri)] (when (nil? new-host) (throw (ProtocolException. (str "Redirect URI does not specify a valid host name: " uri)))))) new-request)) (isRedirected [this request response context] (.isRedirected original request response context))))) (defn get-redirect-strategy [{:keys [redirect-strategy] :as req}] (case redirect-strategy :none (reify RedirectStrategy (getRedirect [this request response context] nil) (isRedirected [this request response context] false)) ;; Like default, but does not throw exceptions when max redirects is ;; reached. :graceful (graceful-redirect-strategy req) :default (default-redirect-strategy DefaultRedirectStrategy/INSTANCE req) :lax (default-redirect-strategy (LaxRedirectStrategy.) req) nil (default-redirect-strategy DefaultRedirectStrategy/INSTANCE req) ;; use directly as reifed RedirectStrategy redirect-strategy)) (defn ^HttpClientBuilder add-retry-handler [^HttpClientBuilder builder handler] (when handler (.setRetryHandler builder (proxy [HttpRequestRetryHandler] [] (retryRequest [e cnt context] (handler e cnt context))))) builder) (defn create-custom-cookie-policy-registry "Given a function that will take an HttpContext and return a CookieSpec, create a new Registry for the cookie policy under the CUSTOM_COOKIE_POLICY string." [cookie-spec-fn] (-> (RegistryBuilder/create) (.register CUSTOM_COOKIE_POLICY (proxy [CookieSpecProvider] [] (create [context] (cookie-spec-fn context)))) (.build))) (defmulti get-cookie-policy "Method to retrieve the cookie policy that should be used for the request. This is a multimethod that may be extended to return your own cookie policy. Dispatches based on the `:cookie-policy` key in the request map." (fn get-cookie-dispatch [request] (:cookie-policy request))) (defmethod get-cookie-policy :none none-cookie-policy [_] CookieSpecs/IGNORE_COOKIES) (defmethod get-cookie-policy :default default-cookie-policy [_] CookieSpecs/DEFAULT) (defmethod get-cookie-policy nil nil-cookie-policy [_] CookieSpecs/DEFAULT) (defmethod get-cookie-policy :netscape netscape-cookie-policy [_] CookieSpecs/NETSCAPE) (defmethod get-cookie-policy :standard standard-cookie-policy [_] CookieSpecs/STANDARD) (defmethod get-cookie-policy :stardard-strict standard-strict-cookie-policy [_] CookieSpecs/STANDARD_STRICT) (defn request-config [{:keys [connection-timeout connection-request-timeout socket-timeout max-redirects cookie-spec normalize-uri ; deprecated conn-request-timeout conn-timeout] :as req}] (let [config (-> (RequestConfig/custom) (.setConnectTimeout (or connection-timeout conn-timeout -1)) (.setSocketTimeout (or socket-timeout -1)) (.setConnectionRequestTimeout (or connection-request-timeout conn-request-timeout -1)) (.setRedirectsEnabled true) (.setCircularRedirectsAllowed (boolean (opt req :allow-circular-redirects))) (.setRelativeRedirectsAllowed ((complement false?) (opt req :allow-relative-redirects))))] (if cookie-spec (.setCookieSpec config CUSTOM_COOKIE_POLICY) (.setCookieSpec config (get-cookie-policy req))) (when max-redirects (.setMaxRedirects config max-redirects)) (when-not (nil? normalize-uri) (.setNormalizeUri config normalize-uri)) (.build config))) (defmulti ^:private construct-http-host (fn [proxy-host proxy-port] (class proxy-host))) (defmethod construct-http-host String [^String proxy-host ^Long proxy-port] (if proxy-port (HttpHost. proxy-host proxy-port) (HttpHost. proxy-host))) (defmethod construct-http-host java.net.InetAddress [^InetAddress proxy-host ^Long proxy-port] (if proxy-port (HttpHost. proxy-host proxy-port) (HttpHost. proxy-host))) (defn ^HttpRoutePlanner get-route-planner "Return an HttpRoutePlanner that either use the supplied proxy settings if any, or the JVM/system proxy settings otherwise" [^String proxy-host ^Long proxy-port proxy-ignore-hosts http-url] (let [ignore-proxy? (and http-url (contains? (set proxy-ignore-hosts) (.getHost (URL. http-url))))] (if (and proxy-host (not ignore-proxy?)) (DefaultProxyRoutePlanner. (construct-http-host proxy-host proxy-port)) (SystemDefaultRoutePlanner. (ProxySelector/getDefault))))) (defn build-cache-config "Given a request with :cache-config as a map or a CacheConfig object, return a CacheConfig object, or nil if no cache config is found. If :cache-config is a map, it checks for the following options: - :allow-303-caching - :asynchronous-worker-idle-lifetime-secs - :asynchronous-workers-core - :asynchronous-workers-max - :heuristic-caching-enabled - :heuristic-coefficient - :heuristic-default-lifetime - :max-cache-entries - :max-object-size - :max-update-retries - :never-cache-http10-responses-with-query-string - :revalidation-queue-size - :shared-cache - :weak-etag-on-put-delete-allowed" [request] (when-let [cc (:cache-config request)] (if (instance? CacheConfig cc) cc (let [config (CacheConfig/custom) {:keys [allow-303-caching asynchronous-worker-idle-lifetime-secs asynchronous-workers-core asynchronous-workers-max heuristic-caching-enabled heuristic-coefficient heuristic-default-lifetime max-cache-entries max-object-size max-update-retries never-cache-http10-responses-with-query-string revalidation-queue-size shared-cache weak-etag-on-put-delete-allowed]} cc] (when (instance? Boolean allow-303-caching) (.setAllow303Caching config allow-303-caching)) (when asynchronous-worker-idle-lifetime-secs (.setAsynchronousWorkerIdleLifetimeSecs config asynchronous-worker-idle-lifetime-secs)) (when asynchronous-workers-core (.setAsynchronousWorkersCore config asynchronous-workers-core)) (when asynchronous-workers-max (.setAsynchronousWorkersMax config asynchronous-workers-max)) (when (instance? Boolean heuristic-caching-enabled) (.setHeuristicCachingEnabled config heuristic-caching-enabled)) (when heuristic-coefficient (.setHeuristicCoefficient config heuristic-coefficient)) (when heuristic-default-lifetime (.setHeuristicDefaultLifetime config heuristic-default-lifetime)) (when max-cache-entries (.setMaxCacheEntries config max-cache-entries)) (when max-object-size (.setMaxObjectSize config max-object-size)) (when max-update-retries (.setMaxUpdateRetries config max-update-retries)) ;; I would add this option, but there is a bug in 4.x CacheConfig that ;; it does not actually correctly use the object from the builder. ;; It's fixed in 5.0 however ;; (when (boolean? never-cache-http10-responses-with-query-string) ;; (.setNeverCacheHTTP10ResponsesWithQueryString ;; config never-cache-http10-responses-with-query-string)) (when revalidation-queue-size (.setRevalidationQueueSize config revalidation-queue-size)) (when (instance? Boolean shared-cache) (.setSharedCache config shared-cache)) (when (instance? Boolean weak-etag-on-put-delete-allowed) (.setWeakETagOnPutDeleteAllowed config weak-etag-on-put-delete-allowed)) (.build config))))) (defn build-http-client "Builds an Apache `HttpClient` from a clj-http request map. Optional arguments `http-url` and `proxy-ignore-hosts` are used to specify the host and a list of hostnames to ignore for any proxy settings. They can be safely ignored if not using proxies." [{:keys [retry-handler request-interceptor response-interceptor proxy-host proxy-port http-builder-fns cookie-spec cookie-policy-registry ^HttpClientBuilder http-client-builder] :as req} caching? conn-mgr & [http-url proxy-ignore-hosts]] ;; have to let first, otherwise we get a reflection warning on (.build) (let [cache? (opt req :cache) builder (-> (cond http-client-builder http-client-builder caching? ^HttpClientBuilder (CachingHttpClientBuilder/create) :else ^HttpClientBuilder (HttpClients/custom)) (.setConnectionManager conn-mgr) (.setRedirectStrategy (get-redirect-strategy req)) (add-retry-handler retry-handler) ;; prefer using clj-http.client/wrap-decompression ;; for consistency between sync/async client options (.disableContentCompression) ;; By default, get the proxy settings ;; from the jvm or system properties (.setRoutePlanner (get-route-planner proxy-host proxy-port proxy-ignore-hosts http-url)))] (when cache? (.setCacheConfig ^CachingHttpClientBuilder builder (build-cache-config req))) (when (or cookie-policy-registry cookie-spec) (if cookie-policy-registry ;; They have a custom registry they'd like to re-use, so use that (.setDefaultCookieSpecRegistry builder cookie-policy-registry) ;; They have only a one-time function for cookie spec, so use that (.setDefaultCookieSpecRegistry builder (create-custom-cookie-policy-registry cookie-spec)))) (when request-interceptor (.addInterceptorLast builder (proxy [HttpRequestInterceptor] [] (process [req ctx] (request-interceptor req ctx))))) (when response-interceptor (.addInterceptorLast builder (proxy [HttpResponseInterceptor] [] (process [resp ctx] (response-interceptor resp ctx))))) (doseq [http-builder-fn http-builder-fns] (http-builder-fn builder req)) (.build builder))) (defn build-async-http-client "Builds an Apache `HttpAsyncClient` from a clj-http request map. Optional arguments `http-url` and `proxy-ignore-hosts` are used to specify the host and a list of hostnames to ignore for any proxy settings. They can be safely ignored if not using proxies." [{:keys [request-interceptor response-interceptor proxy-host proxy-port async-http-builder-fns] :as req} conn-mgr & [http-url proxy-ignore-hosts]] ;; have to let first, otherwise we get a reflection warning on (.build) (let [^HttpAsyncClientBuilder builder (-> (HttpAsyncClients/custom) (.setConnectionManager conn-mgr) (.setRedirectStrategy (get-redirect-strategy req)) ;; By default, get the proxy ;; settings from the jvm or system ;; properties (.setRoutePlanner (get-route-planner proxy-host proxy-port proxy-ignore-hosts http-url)))] (when (conn/reusable? conn-mgr) (.setConnectionManagerShared builder true)) (when request-interceptor (.addInterceptorLast builder (proxy [HttpRequestInterceptor] [] (process [req ctx] (request-interceptor req ctx))))) (when response-interceptor (.addInterceptorLast builder (proxy [HttpResponseInterceptor] [] (process [resp ctx] (response-interceptor resp ctx))))) (doseq [async-http-builder-fn async-http-builder-fns] (async-http-builder-fn builder req)) (.build builder))) (defn http-get [] (HttpGet. "https://www.google.com")) (defn make-proxy-method-with-body [method] (fn [url] (doto (proxy [HttpEntityEnclosingRequestBase] [] (getMethod [] (.toUpperCase (name method) Locale/ROOT))) (.setURI (URI. url))))) (def proxy-delete-with-body (make-proxy-method-with-body :delete)) (def proxy-get-with-body (make-proxy-method-with-body :get)) (def proxy-copy-with-body (make-proxy-method-with-body :copy)) (def proxy-move-with-body (make-proxy-method-with-body :move)) (def proxy-patch-with-body (make-proxy-method-with-body :patch)) (def ^:dynamic *cookie-store* nil) (defn make-proxy-method [method url] (doto (proxy [HttpRequestBase] [] (getMethod [] (str method))) (.setURI (URI/create url)))) (defn http-request-for "Provides the HttpRequest object for a particular request-method and url" [request-method ^String http-url body] (case request-method :get (if body (proxy-get-with-body http-url) (HttpGet. http-url)) :head (HttpHead. http-url) :put (HttpPut. http-url) :post (HttpPost. http-url) :options (HttpOptions. http-url) :delete (if body (proxy-delete-with-body http-url) (HttpDelete. http-url)) :copy (proxy-copy-with-body http-url) :move (proxy-move-with-body http-url) :patch (if body (proxy-patch-with-body http-url) (HttpPatch. http-url)) (if body ((make-proxy-method-with-body request-method) http-url) (make-proxy-method request-method http-url)))) (defn ^HttpClientContext http-context [caching? request-config http-client-context] (let [^HttpClientContext typed-context (or http-client-context (if caching? (HttpCacheContext/create) (HttpClientContext/create)))] (doto typed-context (.setRequestConfig request-config)))) (defn ^CredentialsProvider credentials-provider [] (BasicCredentialsProvider.)) (defn- coerce-body-entity "Coerce the http-entity from an HttpResponse to a stream that closes itself and the connection manager when closed." [^HttpEntity http-entity conn-mgr ^CloseableHttpResponse response] (if http-entity (proxy [FilterInputStream] [^InputStream (.getContent http-entity)] (close [] (try ;; Eliminate the reflection warning from proxy-super (let [^InputStream this this] (proxy-super close)) (finally (when (instance? CloseableHttpResponse response) (.close response)) (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)))))) (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)))) (defn- print-debug! "Print out debugging information to *out* for a given request." [{:keys [debug-body body] :as req} http-req] (println (with-out-str (println "Request:" (type body)) (clojure.pprint/pprint (assoc req :body (if (opt req :debug-body) (cond (isa? (type body) String) body (isa? (type body) HttpEntity) (let [baos (ByteArrayOutputStream.)] (.writeTo ^HttpEntity body baos) (.toString baos "UTF-8")) :else nil) (if (isa? (type body) String) (format "... %s bytes ..." (count body)) (and body (bean body)))) :body-type (type body))) (println "HttpRequest:") (clojure.pprint/pprint (bean http-req))))) (defn- build-response-map [^HttpResponse response req ^HttpUriRequest http-req http-url conn-mgr ^HttpClientContext context ^CloseableHttpClient client] (let [^HttpEntity entity (.getEntity response) status (.getStatusLine response) protocol-version (.getProtocolVersion status) body (:body req) response {:body (coerce-body-entity entity conn-mgr response) :http-client client :headers (parse-headers (.headerIterator response) (opt req :use-header-maps-in-response)) :length (if (nil? entity) 0 (.getContentLength entity)) :chunked? (if (nil? entity) false (.isChunked entity)) :repeatable? (if (nil? entity) false (.isRepeatable entity)) :streaming? (if (nil? entity) false (.isStreaming entity)) :status (.getStatusCode status) :protocol-version {:name (.getProtocol protocol-version) :major (.getMajor protocol-version) :minor (.getMinor protocol-version)} :reason-phrase (.getReasonPhrase status) :trace-redirects (mapv str (.getRedirectLocations context)) :cached (when (instance? HttpCacheContext context) (when-let [cache-resp (.getCacheResponseStatus ^HttpCacheContext context)] (-> cache-resp str keyword)))}] (if (opt req :save-request) (-> response (assoc :request req) (assoc-in [:request :body-type] (type body)) (assoc-in [:request :http-url] http-url) (update-in [:request] #(if (opt req :debug-body) (assoc % :body-content (cond (isa? (type (:body %)) String) (:body %) (isa? (type (:body %)) HttpEntity) (let [baos (ByteArrayOutputStream.)] (.writeTo ^HttpEntity (:body %) baos) (.toString baos "UTF-8")) :else nil)) %)) (assoc-in [:request :http-req] http-req)) response))) (defn- get-conn-mgr [async? req] (if async? (or conn/*async-connection-manager* (conn/make-regular-async-conn-manager req)) (or conn/*connection-manager* (conn/make-regular-conn-manager req)))) (defn request ([req] (request req nil nil)) ([{:keys [body connection-timeout connection-request-timeout connection-manager cookie-store cookie-policy headers multipart query-string redirect-strategy max-redirects retry-handler request-method scheme server-name server-port socket-timeout uri response-interceptor proxy-host proxy-port http-client-context http-request-config http-client proxy-ignore-hosts proxy-user proxy-pass digest-auth ntlm-auth multipart-mode multipart-charset ; deprecated conn-timeout conn-request-timeout] :as req} respond raise] (let [async? (opt req :async) cache? (opt req :cache) scheme (name scheme) http-url (str scheme "://" server-name (when server-port (str ":" server-port)) uri (when query-string (str "?" query-string))) conn-mgr (or connection-manager (get-conn-mgr async? req)) proxy-ignore-hosts (or proxy-ignore-hosts #{"localhost" "127.0.0.1"}) ^RequestConfig request-config (or http-request-config (request-config req)) ^HttpClientContext context (http-context cache? request-config http-client-context) ^HttpUriRequest http-req (http-request-for request-method http-url body)] (when-not (conn/reusable? conn-mgr) (.addHeader http-req "Connection" "close")) (when-let [cookie-jar (or cookie-store *cookie-store*)] (.setCookieStore context cookie-jar)) (when-let [[user pass] digest-auth] (.setCredentialsProvider context (doto (credentials-provider) (.setCredentials (AuthScope. nil -1 nil) (UsernamePasswordCredentials. user pass))))) (when-let [[user password host domain] ntlm-auth] (.setCredentialsProvider context (doto (credentials-provider) (.setCredentials (AuthScope. nil -1 nil) (NTCredentials. user password host domain))))) (when (and proxy-user proxy-pass) (let [authscope (AuthScope. proxy-host proxy-port) creds (UsernamePasswordCredentials. proxy-user proxy-pass)] (.setCredentialsProvider context (doto (credentials-provider) (.setCredentials authscope creds))))) (if multipart (.setEntity ^HttpEntityEnclosingRequest http-req (mp/create-multipart-entity multipart req)) (when (and body (instance? HttpEntityEnclosingRequest http-req)) (if (instance? HttpEntity body) (.setEntity ^HttpEntityEnclosingRequest http-req body) (.setEntity ^HttpEntityEnclosingRequest http-req (if (string? body) (StringEntity. ^String body "UTF-8") (ByteArrayEntity. body)))))) (doseq [[header-n header-v] headers] (if (coll? header-v) (doseq [header-vth header-v] (.addHeader http-req header-n header-vth)) (.addHeader http-req header-n (str header-v)))) (when (opt req :debug) (print-debug! req http-req)) (if-not async? (let [^CloseableHttpClient client (or http-client (build-http-client req cache? conn-mgr http-url proxy-ignore-hosts))] (try (build-response-map (.execute client http-req context) req http-req http-url conn-mgr context client) (catch Throwable t (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)) (throw t)))) (let [^CloseableHttpAsyncClient client (or http-client (build-async-http-client req conn-mgr http-url proxy-ignore-hosts)) original-thread-bindings (clojure.lang.Var/getThreadBindingFrame)] (when cache? (throw (IllegalArgumentException. "caching is not yet supported for async clients"))) (.start client) (.execute client http-req context (reify org.apache.http.concurrent.FutureCallback (failed [this ex] (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings) (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)) (if (opt req :ignore-unknown-host) ((:unknown-host-respond req) nil) (raise ex))) (completed [this resp] (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings) (try (respond (build-response-map resp req http-req http-url conn-mgr context client)) (catch Throwable t (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)) (raise t)))) (cancelled [this] (clojure.lang.Var/resetThreadBindingFrame original-thread-bindings) ;; Run the :oncancel function if available (when-let [oncancel (:oncancel req)] (oncancel)) ;; Attempt to abort the execution of the request (.abort http-req) (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)))))))))) clj-http-3.12.3/src/clj_http/core_old.clj000066400000000000000000000330461407075231000202100ustar00rootroot00000000000000(ns clj-http.core-old "Core HTTP request/response implementation." (:require [clj-http.conn-mgr :as conn] [clj-http.headers :as headers] [clj-http.multipart :as mp] [clj-http.util :refer [opt]] clojure.pprint) (:import [java.io ByteArrayOutputStream FilterInputStream InputStream] java.net.URI [org.apache.http HeaderIterator HttpEntity HttpEntityEnclosingRequest HttpHost HttpResponseInterceptor] [org.apache.http.auth AuthScope NTCredentials UsernamePasswordCredentials] [org.apache.http.client HttpClient HttpRequestRetryHandler] [org.apache.http.client.methods HttpDelete HttpEntityEnclosingRequestBase HttpGet HttpHead HttpOptions HttpPatch HttpPost HttpPut HttpUriRequest] [org.apache.http.client.params ClientPNames CookiePolicy] org.apache.http.conn.ClientConnectionManager org.apache.http.conn.params.ConnRoutePNames org.apache.http.conn.routing.HttpRoute org.apache.http.cookie.CookieSpecFactory org.apache.http.cookie.params.CookieSpecPNames [org.apache.http.entity ByteArrayEntity StringEntity] org.apache.http.impl.client.DefaultHttpClient org.apache.http.impl.conn.ProxySelectorRoutePlanner org.apache.http.impl.cookie.BrowserCompatSpec org.apache.http.params.CoreConnectionPNames)) (defn parse-headers "Takes a HeaderIterator and returns a map of names to values. If a name appears more than once (like `set-cookie`) then the value will be a vector containing the values in the order they appeared in the headers." [^HeaderIterator headers & [use-header-maps-in-response?]] (if-not use-header-maps-in-response? (->> (headers/header-iterator-seq headers) (map (fn [[k v]] [(.toLowerCase ^String k) v])) (reduce (fn [hs [k v]] (headers/assoc-join hs k v)) {})) (->> (headers/header-iterator-seq headers) (reduce (fn [hs [k v]] (headers/assoc-join hs k v)) (headers/header-map))))) (defn set-client-param [^HttpClient client key val] (when-not (nil? val) (-> client (.getParams) (.setParameter key val)))) (defn make-proxy-method-with-body [method] (fn [^String url] (doto (proxy [HttpEntityEnclosingRequestBase] [] (getMethod [] (.toUpperCase (name method)))) (.setURI (URI. url))))) (def proxy-delete-with-body (make-proxy-method-with-body :delete)) (def proxy-get-with-body (make-proxy-method-with-body :get)) (def proxy-copy-with-body (make-proxy-method-with-body :copy)) (def proxy-move-with-body (make-proxy-method-with-body :move)) (def proxy-patch-with-body (make-proxy-method-with-body :patch)) (def ^:dynamic *cookie-store* nil) (defn- set-routing "Use ProxySelectorRoutePlanner to choose proxy sensible based on http.nonProxyHosts" [^DefaultHttpClient client] (.setRoutePlanner client (ProxySelectorRoutePlanner. (.. client getConnectionManager getSchemeRegistry) nil)) client) (defn maybe-force-proxy [^DefaultHttpClient client ^HttpEntityEnclosingRequestBase request proxy-host proxy-port proxy-ignore-hosts] (let [uri (.getURI request)] (when (and (nil? ((set proxy-ignore-hosts) (.getHost uri))) proxy-host) (let [target (HttpHost. (.getHost uri) (.getPort uri) (.getScheme uri)) route (HttpRoute. target nil (HttpHost. ^String proxy-host (int proxy-port)) (.. client getConnectionManager getSchemeRegistry (getScheme target) isLayered))] (set-client-param client ConnRoutePNames/FORCED_ROUTE route))) request)) (defn cookie-spec "Create an instance of a org.apache.http.impl.cookie.BrowserCompatSpec with a validate function that you pass in. This function takes two parameters, a cookie and an origin." [f] (proxy [BrowserCompatSpec] [] (validate [cookie origin] (f cookie origin)))) (defn cookie-spec-factory "Create an instance of a org.apache.http.cookie.CookieSpecFactory with a newInstance implementation that returns a cookie specification with a validate function that you pass in. The function takes two parameters: cookie and origin." [f] (proxy [CookieSpecFactory] [] (newInstance [params] (cookie-spec f)))) (defn add-client-params! "Add various client params to the http-client object, if needed." [^DefaultHttpClient http-client kvs] (let [cookie-policy (:cookie-policy kvs) cookie-policy-name (str (type cookie-policy)) kvs (dissoc kvs :cookie-policy)] (when cookie-policy (-> http-client .getCookieSpecs (.register cookie-policy-name (cookie-spec-factory cookie-policy)))) (doto http-client (set-client-param ClientPNames/COOKIE_POLICY (if cookie-policy cookie-policy-name CookiePolicy/BROWSER_COMPATIBILITY)) (set-client-param CookieSpecPNames/SINGLE_COOKIE_HEADER true) (set-client-param ClientPNames/HANDLE_REDIRECTS false)) (doseq [[k v] kvs] (set-client-param http-client k (cond (and (not= ClientPNames/CONN_MANAGER_TIMEOUT k) (instance? Long v)) (Integer. ^Long v) true v))))) (defn- coerce-body-entity "Coerce the http-entity from an HttpResponse to either a byte-array, or a stream that closes itself and the connection manager when closed." [{:keys [as]} ^HttpEntity http-entity ^ClientConnectionManager conn-mgr] (if http-entity (proxy [FilterInputStream] [^InputStream (.getContent http-entity)] (close [] (try ;; Eliminate the reflection warning from proxy-super (let [^InputStream this this] (proxy-super close)) (finally (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)))))) (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)))) (defn- print-debug! "Print out debugging information to *out* for a given request." [{:keys [debug-body body] :as req} http-req] (println "Request:" (type body)) (clojure.pprint/pprint (assoc req :body (if (opt req :debug-body) (cond (isa? (type body) String) body (isa? (type body) HttpEntity) (let [baos (ByteArrayOutputStream.)] (.writeTo ^HttpEntity body baos) (.toString baos "UTF-8")) :else nil) (if (isa? (type body) String) (format "... %s bytes ..." (count body)) (and body (bean body)))) :body-type (type body))) (println "HttpRequest:") (clojure.pprint/pprint (bean http-req))) (defn http-request-for "Provides the HttpRequest object for a particular request-method and url" [request-method ^String http-url body] (case request-method :get (if body (proxy-get-with-body http-url) (HttpGet. http-url)) :head (HttpHead. http-url) :put (HttpPut. http-url) :post (HttpPost. http-url) :options (HttpOptions. http-url) :delete (if body (proxy-delete-with-body http-url) (HttpDelete. http-url)) :copy (proxy-copy-with-body http-url) :move (proxy-move-with-body http-url) :patch (if body (proxy-patch-with-body http-url) (HttpPatch. http-url)) (throw (IllegalArgumentException. (str "Invalid request method " request-method))))) (defn request "Executes the HTTP request corresponding to the given Ring request map and returns the Ring response map corresponding to the resulting HTTP response. Note that where Ring uses InputStreams for the request and response bodies, the clj-http uses ByteArrays for the bodies." [{:keys [request-method scheme server-name server-port uri query-string headers body multipart socket-timeout connection-timeout proxy-host proxy-ignore-hosts proxy-port proxy-user proxy-pass as cookie-store retry-handler response-interceptor digest-auth ntlm-auth connection-manager client-params ; deprecated conn-timeout ] :as req}] (let [^ClientConnectionManager conn-mgr (or connection-manager conn/*connection-manager* (conn/make-regular-conn-manager req)) ^DefaultHttpClient http-client (set-routing (DefaultHttpClient. conn-mgr)) scheme (name scheme)] (when-let [cookie-store (or cookie-store *cookie-store*)] (.setCookieStore http-client cookie-store)) (when retry-handler (.setHttpRequestRetryHandler http-client (proxy [HttpRequestRetryHandler] [] (retryRequest [e cnt context] (retry-handler e cnt context))))) (add-client-params! http-client ;; merge in map of specified timeouts, to ;; support backward compatibility. (merge {CoreConnectionPNames/SO_TIMEOUT socket-timeout CoreConnectionPNames/CONNECTION_TIMEOUT (or connection-timeout conn-timeout)} client-params)) (when-let [[user pass] digest-auth] (.setCredentials (.getCredentialsProvider http-client) (AuthScope. nil -1 nil) (UsernamePasswordCredentials. user pass))) (when-let [[user password host domain] ntlm-auth] (.setCredentials (.getCredentialsProvider http-client) (AuthScope. nil -1 nil) (NTCredentials. user password host domain))) (when (and proxy-user proxy-pass) (let [authscope (AuthScope. proxy-host proxy-port) creds (UsernamePasswordCredentials. proxy-user proxy-pass)] (.setCredentials (.getCredentialsProvider http-client) authscope creds))) (let [http-url (str scheme "://" server-name (when server-port (str ":" server-port)) uri (when query-string (str "?" query-string))) req (assoc req :http-url http-url) proxy-ignore-hosts (or proxy-ignore-hosts #{"localhost" "127.0.0.1"}) ^HttpUriRequest http-req (maybe-force-proxy http-client (http-request-for request-method http-url body) proxy-host proxy-port proxy-ignore-hosts)] (when response-interceptor (.addResponseInterceptor http-client (proxy [HttpResponseInterceptor] [] (process [resp ctx] (response-interceptor resp ctx))))) (when-not (conn/reusable? conn-mgr) (.addHeader http-req "Connection" "close")) (doseq [[header-n header-v] headers] (if (coll? header-v) (doseq [header-vth header-v] (.addHeader http-req header-n header-vth)) (.addHeader http-req header-n (str header-v)))) (if multipart (.setEntity ^HttpEntityEnclosingRequest http-req (mp/create-multipart-entity multipart req)) (when (and body (instance? HttpEntityEnclosingRequest http-req)) (if (instance? HttpEntity body) (.setEntity ^HttpEntityEnclosingRequest http-req body) (.setEntity ^HttpEntityEnclosingRequest http-req (if (string? body) (StringEntity. ^String body "UTF-8") (ByteArrayEntity. body)))))) (when (opt req :debug) (print-debug! req http-req)) (try (let [http-resp (.execute http-client http-req) http-entity (.getEntity http-resp) resp {:status (.getStatusCode (.getStatusLine http-resp)) :headers (parse-headers (.headerIterator http-resp) (opt req :use-header-maps-in-response)) :body (coerce-body-entity req http-entity conn-mgr)}] (if (opt req :save-request) (-> resp (assoc :request req) (assoc-in [:request :body-type] (type body)) (update-in [:request] #(if (opt req :debug-body) (assoc % :body-content (cond (isa? (type (:body %)) String) (:body %) (isa? (type (:body %)) HttpEntity) (let [baos (ByteArrayOutputStream.)] (.writeTo ^HttpEntity (:body %) baos) (.toString baos "UTF-8")) :else nil)) %)) (assoc-in [:request :http-req] http-req) (dissoc :save-request?)) resp)) (catch Throwable e (when-not (conn/reusable? conn-mgr) (conn/shutdown-manager conn-mgr)) (throw e)))))) clj-http-3.12.3/src/clj_http/headers.clj000066400000000000000000000113251407075231000200310ustar00rootroot00000000000000(ns clj-http.headers "Provides wrap-header-map, which is middleware allows headers to be specified more flexibly. In requests and responses, headers can be accessed as strings or keywords of any case. In requests, string header names will be sent to the server with their casing unchanged, while keyword header names will be transformed into their canonical HTTP representation (e.g. :accept-encoding will become \"Accept-Encoding\")." (:require [clojure.string :as s] [potemkin :as potemkin]) (:import java.util.Locale [org.apache.http Header HeaderIterator])) (def special-cases "A collection of HTTP headers that do not follow the normal Looks-Like-This casing." ["Content-MD5" "DNT" "ETag" "P3P" "TE" "WWW-Authenticate" "X-ATT-DeviceId" "X-UA-Compatible" "X-WebKit-CSP" "X-XSS-Protection"]) (defn special-case "Returns the special-case capitalized version of a string if that string is a special case, otherwise returns the string unchanged." [^String s] (or (first (filter #(.equalsIgnoreCase ^String % s) special-cases)) s)) (defn ^String lower-case "Converts a string to all lower-case, using the root locale. Warning: This is not a general purpose lower-casing function -- it is useful for case-insensitive comparisons of strings, not for converting a string into something that's useful for humans." [^CharSequence s] (when s (.toLowerCase (.toString s) Locale/ROOT))) (defn title-case "Converts a character to titlecase." [^Character c] (when c (Character/toTitleCase c))) (defn canonicalize "Transforms a keyword header name into its canonical string representation. The canonical string representation is title-cased words separated by dashes, like so: :date -> \"Date\", :DATE -> \"Date\", and :foo-bar -> \"Foo-Bar\". However, there is special-casing for some common headers, so: :p3p -> \"P3P\", and :content-md5 -> \"Content-MD5\"." [k] (when k (-> (name k) (lower-case) (s/replace #"(?:^.|-.)" (fn [s] (if (next s) (str (first s) (title-case (second s))) (str (title-case (first s)))))) (special-case)))) (defn normalize "Turns a string or keyword into normalized form, which is a lowercase string." [k] (when k (lower-case (name k)))) (defn header-iterator-seq "Takes a HeaderIterator and returns a seq of vectors of name/value pairs of headers." [^HeaderIterator headers] (for [^Header h (iterator-seq headers)] [(.getName h) (.getValue h)])) (defn assoc-join "Like assoc, but will join multiple values into a vector if the given key is already present into the map." [headers name value] (update-in headers [name] (fn [existing] (cond (vector? existing) (conj existing value) (nil? existing) value :else [existing value])))) ;; a map implementation that stores both the original (or canonical) ;; key and value for each key/value pair, but performs lookups and ;; other operations using the normalized -- this allows a value to be ;; looked up by many similar keys, and not just the exact precise key ;; it was originally stored with. (potemkin/def-map-type HeaderMap [m mta] (get [_ k v] (second (get m (normalize k) [nil v]))) (assoc [_ k v] (HeaderMap. (assoc m (normalize k) [(if (keyword? k) (canonicalize k) k) v]) mta)) (dissoc [_ k] (HeaderMap. (dissoc m (normalize k)) mta)) (keys [_] (map first (vals m))) (meta [_] mta) (with-meta [_ mta] (HeaderMap. m mta)) clojure.lang.Associative (containsKey [_ k] (contains? m (normalize k))) (entryAt [_ k] (if (contains? m (normalize k)) (clojure.lang.MapEntry. k (get _ k)))) (empty [_] (HeaderMap. {} nil))) (defn header-map "Returns a new header map with supplied mappings." [& keyvals] (into (HeaderMap. {} nil) (apply array-map keyvals))) (defn- header-map-request [req] (let [req-headers (:headers req)] (if req-headers (-> req (assoc :headers (into (header-map) req-headers) :use-header-maps-in-response? true)) req))) (defn wrap-header-map "Middleware that converts headers from a map into a header-map." [client] (fn ([req] (client (header-map-request req))) ([req respond raise] (client (header-map-request req) respond raise)))) clj-http-3.12.3/src/clj_http/links.clj000066400000000000000000000035631407075231000175430ustar00rootroot00000000000000(ns clj-http.links "Namespace dealing with HTTP link headers") (def ^:private quoted-string #"\"((?:[^\"]|\\\")*)\"") (def ^:private token #"([^,\";]*)") (def ^:private link-param (re-pattern (str "(\\w+)=(?:" quoted-string "|" token ")"))) (def ^:private uri-reference #"<([^>]*)>") (def ^:private link-value (re-pattern (str uri-reference "((?:\\s*;\\s*" link-param ")*)"))) (def ^:private link-header (re-pattern (str "(?:\\s*(" link-value ")\\s*,?\\s*)"))) (defn read-link-params [params] (into {} (for [[_ name quot tok] (re-seq link-param params)] [(keyword name) (or quot tok)]))) (defn read-link-value [value] (let [[_ uri params] (re-matches link-value value) param-map (read-link-params params)] [(keyword (:rel param-map)) (-> param-map (assoc :href uri) (dissoc :rel))])) (defn read-link-headers [header] (->> (re-seq link-header header) (map second) (map read-link-value) (into {}))) (defn- links-response [response] (if-let [link-headers (get-in response [:headers "link"])] (let [link-headers (if (coll? link-headers) link-headers [link-headers])] (assoc response :links (into {} (map read-link-headers link-headers)))) response)) (defn wrap-links "Add a :links key to the response map that contains parsed Link headers. The links will be represented as a map, with the 'rel' value being the key. The URI is placed under the 'href' key, to mimic the HTML link element. e.g. Link: ; rel=next; title=\"Page 2\" => {:links {:next {:href \"http://example.com/page2.html\" :title \"Page 2\"}}}" [client] (fn ([request] (links-response (client request))) ([request respond raise] (client request #(respond (links-response %)) raise)))) clj-http-3.12.3/src/clj_http/multipart.clj000066400000000000000000000140641407075231000204420ustar00rootroot00000000000000(ns clj-http.multipart "Namespace used for clj-http to create multipart entities and bodies." (:import [java.io File InputStream] org.apache.http.Consts org.apache.http.entity.ContentType [org.apache.http.entity.mime HttpMultipartMode MultipartEntityBuilder] [org.apache.http.entity.mime.content ByteArrayBody ContentBody FileBody InputStreamBody StringBody])) ;; we don't need to make a fake byte-array every time, only once (def byte-array-type (type (byte-array 0))) (defmulti make-multipart-body "Create a body object from the given map, dispatching on the type of its content. By default supported content body types are: - String - byte array (requires providing name) - InputStream (requires providing name) - File - org.apache.http.entity.mime.content.ContentBody (which is just returned)" (fn [multipart] (type (:content multipart)))) (defmethod make-multipart-body nil [multipart] (throw (Exception. "Multipart content cannot be nil"))) (defmethod make-multipart-body :default [multipart] (throw (Exception. (str "Unsupported type for multipart content: " (type (:content multipart)))))) (defmethod make-multipart-body File ;; Create a FileBody object from the given map, requiring at least :content [{:keys [^String name ^String mime-type ^File content ^String encoding]}] (cond (and name mime-type content encoding) (FileBody. content (ContentType/create mime-type encoding) name) (and mime-type content encoding) (FileBody. content (ContentType/create mime-type encoding)) (and name mime-type content) (FileBody. content (ContentType/create mime-type) name) (and mime-type content) (FileBody. content (ContentType/create mime-type)) content (FileBody. content) :else (throw (Exception. "Multipart file body must contain at least :content")))) (defmethod make-multipart-body InputStream ;; Create an InputStreamBody object from the given map, requiring at least ;; :content and :name. If no :length is specified, clj-http will use ;; chunked transfer-encoding, if :length is specified, clj-http will ;; workaround things be proxying the InputStreamBody to return a length. [{:keys [^String name ^String mime-type ^InputStream content length]}] (cond (and content name length) (if mime-type (proxy [InputStreamBody] [content (ContentType/create mime-type) name] (getContentLength [] length)) (proxy [InputStreamBody] [content name] (getContentLength [] length))) (and content mime-type name) (InputStreamBody. content (ContentType/create mime-type) name) (and content name) (InputStreamBody. content name) :else (throw (Exception. (str "Multipart input stream body must contain " "at least :content and :name"))))) (defmethod make-multipart-body byte-array-type ;; Create a ByteArrayBody object from the given map, requiring at least ;; :content and :name. [{:keys [^String name ^String mime-type ^bytes content]}] (cond (and content name mime-type) (ByteArrayBody. content (ContentType/create mime-type) name) (and content name) (ByteArrayBody. content name) :else (throw (Exception. (str "Multipart byte array body must contain " "at least :content and :name"))))) (defmulti ^java.nio.charset.Charset encoding-to-charset class) (defmethod encoding-to-charset nil [encoding] nil) (defmethod encoding-to-charset java.nio.charset.Charset [encoding] encoding) (defmethod encoding-to-charset java.lang.String [encoding] (java.nio.charset.Charset/forName encoding)) (defmethod make-multipart-body String ;; Create a StringBody object from the given map, requiring at least :content. ;; If :encoding is specified, it will be created using the Charset for that ;; encoding. [{:keys [^String mime-type ^String content encoding]}] (cond (and content mime-type encoding) (StringBody. content (ContentType/create mime-type (encoding-to-charset encoding))) (and content encoding) (StringBody. content (ContentType/create "text/plain" (encoding-to-charset encoding))) content (StringBody. content (ContentType/create "text/plain" Consts/UTF_8)))) (defmethod make-multipart-body ContentBody ;; Use provided org.apache.http.entity.mime.content.ContentBody directly [{:keys [^ContentBody content]}] content) (defn- multipart-workaround "Workaround for AsyncHttpClient to bypass 25kb restriction on getContent. See https://github.com/dakrone/clj-http/issues/560. " [^org.apache.http.entity.mime.MultipartFormEntity mp-entity] (reify org.apache.http.HttpEntity (isRepeatable [_] (.isRepeatable mp-entity)) (isChunked [_] (.isChunked mp-entity)) (isStreaming [_] (.isStreaming mp-entity)) (getContentLength [_] (.getContentLength mp-entity)) (getContentType [_] (.getContentType mp-entity)) (getContentEncoding [_] (.getContentEncoding mp-entity)) (consumeContent [_] (.consumeContent mp-entity)) (getContent [_] (let [os (java.io.ByteArrayOutputStream.)] (.writeTo mp-entity os) (.flush os) (java.io.ByteArrayInputStream. (.toByteArray os)))) (writeTo [_ output-stream] (.writeTo mp-entity output-stream)))) (defn create-multipart-entity "Takes a multipart vector of maps and creates a MultipartEntity with each map added as a part, depending on the type of content." [multipart {:keys [mime-subtype multipart-mode multipart-charset] :or {mime-subtype "form-data" multipart-mode HttpMultipartMode/STRICT}}] (let [mp-entity (doto (MultipartEntityBuilder/create) (.setMode multipart-mode) (.setMimeSubtype mime-subtype))] (when multipart-charset (.setCharset mp-entity (encoding-to-charset multipart-charset))) (doseq [m multipart] (let [name (or (:part-name m) (:name m)) part (make-multipart-body m)] (.addPart mp-entity name part))) (multipart-workaround (.build mp-entity)))) clj-http-3.12.3/src/clj_http/util.clj000066400000000000000000000126021407075231000173720ustar00rootroot00000000000000(ns clj-http.util "Helper functions for the HTTP client." (:require [clojure.string :refer [blank? lower-case split trim]] [clojure.walk :refer [postwalk]]) (:import [java.io BufferedInputStream ByteArrayInputStream ByteArrayOutputStream EOFException InputStream PushbackInputStream] [java.net URLDecoder URLEncoder] [java.util.zip DeflaterInputStream GZIPInputStream GZIPOutputStream InflaterInputStream] org.apache.commons.codec.binary.Base64 org.apache.commons.io.IOUtils)) (defn utf8-bytes "Returns the encoding's bytes corresponding to the given string. If no encoding is specified, UTF-8 is used." [^String s & [^String encoding]] (.getBytes s (or encoding "UTF-8"))) (defn utf8-string "Returns the String corresponding to the given encoding's decoding of the given bytes. If no encoding is specified, UTF-8 is used." [^"[B" b & [^String encoding]] (String. b (or encoding "UTF-8"))) (defn url-decode "Returns the form-url-decoded version of the given string, using either a specified encoding or UTF-8 by default." [^String encoded & [^String encoding]] (URLDecoder/decode encoded (or encoding "UTF-8"))) (defn url-encode "Returns an UTF-8 URL encoded version of the given string." [^String unencoded & [^String encoding]] (URLEncoder/encode unencoded (or encoding "UTF-8"))) (defn base64-encode "Encode an array of bytes into a base64 encoded string." [unencoded] (utf8-string (Base64/encodeBase64 unencoded))) (defn gunzip "Returns a gunzip'd version of the given byte array." [b] (when b (cond (instance? InputStream b) (let [^PushbackInputStream b (PushbackInputStream. b) first-byte (int (try (.read b) (catch EOFException _ -1)))] (case first-byte -1 b (do (.unread b first-byte) (GZIPInputStream. b)))) :else (IOUtils/toByteArray (GZIPInputStream. (ByteArrayInputStream. b)))))) (defn gzip "Returns a gzip'd version of the given byte array." [b] (when b (let [baos (ByteArrayOutputStream.) gos (GZIPOutputStream. baos)] (IOUtils/copy (ByteArrayInputStream. b) gos) (.close gos) (.toByteArray baos)))) (defn force-stream "Force b as InputStream if it is a ByteArray." ^InputStream [b] (if (instance? InputStream b) b (ByteArrayInputStream. b))) (defn force-byte-array "force b as byte array if it is an InputStream, also close the stream" ^bytes [b] (if (instance? InputStream b) (let [^PushbackInputStream bs (PushbackInputStream. b)] (try (let [first-byte (int (try (.read bs) (catch EOFException _ -1)))] (case first-byte -1 (byte-array 0) (do (.unread bs first-byte) (IOUtils/toByteArray bs)))) (finally (.close bs)))) b)) (defn force-string "Convert s (a ByteArray or InputStream) to String." ^String [s ^String charset] (if (instance? InputStream s) (let [^PushbackInputStream bs (PushbackInputStream. s)] (try (let [first-byte (int (try (.read bs) (catch EOFException _ -1)))] (case first-byte -1 "" (do (.unread bs first-byte) (IOUtils/toString bs charset)))) (finally (.close bs)))) (IOUtils/toString ^"[B" s charset))) (defn inflate "Returns a zlib inflate'd version of the given byte array or InputStream." [b] (when b ;; This weirdness is because HTTP servers lie about what kind of deflation ;; they're using, so we try one way, then if that doesn't work, reset and ;; try the other way (let [stream (BufferedInputStream. (if (instance? InputStream b) b (ByteArrayInputStream. b))) _ (.mark stream 512) iis (InflaterInputStream. stream) readable? (try (.read iis) true (catch java.util.zip.ZipException _ false))] (.reset stream) (if readable? (InflaterInputStream. stream) (InflaterInputStream. stream (java.util.zip.Inflater. true)))))) (defn deflate "Returns a deflate'd version of the given byte array." [b] (when b (IOUtils/toByteArray (DeflaterInputStream. (ByteArrayInputStream. b))))) (defn lower-case-keys "Recursively lower-case all map keys that are strings." [m] (let [f (fn [[k v]] (if (string? k) [(lower-case k) v] [k v]))] (postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m))) (defn opt "Check the request parameters for a keyword boolean option, with or without the ? Returns false if either of the values are false, or the value of (or key1 key2) otherwise (truthy)" [req param] (let [param-? (keyword (str (name param) "?")) v1 (clojure.core/get req param) v2 (clojure.core/get req param-?)] (if (false? v1) false (if (false? v2) false (or v1 v2))))) (defn- trim-quotes [s] (clojure.string/replace s #"^\s*(\"(.*)\"|(.*?))\s*$" "$2$3")) (defn parse-content-type "Parse `s` as an RFC 2616 media type." [s] (when-let [m (re-matches #"\s*(([^/]+)/([^ ;]+))\s*(\s*;.*)?" (str s))] {:content-type (keyword (nth m 1)) :content-type-params (->> (split (str (nth m 4)) #"\s*;\s*") (remove blank?) (map #(split % #"=")) (mapcat (fn [[k v]] [(keyword (lower-case k)) (trim-quotes v)])) (apply hash-map))})) clj-http-3.12.3/test-resources/000077500000000000000000000000001407075231000163135ustar00rootroot00000000000000clj-http-3.12.3/test-resources/big_array_json.json000066400000000000000000000134171407075231000222040ustar00rootroot00000000000000[ {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]}, {"foo": "bar", "baz": "qux", "values": [1, 2, 3, 4, 5]} ] clj-http-3.12.3/test-resources/client-keystore000066400000000000000000000066071407075231000213700ustar00rootroot000000000000000 0 < *H  - )0 %0i *H ZV0R0N *H  00) *H  0:-QM~]cXOenPRPMHݝx/ NsHRM]gJE>ҶK.;'{)r~|lJ`7F# FinǢ̶B|J9Ggk;,7*K:NG:"^NMY#E很Uz~V(-PؑE LEUĻNL%݌?G!1~gguj~ Vzw]dqڱl Wf$Cq@dKÙ%$9d#V{!UQ)ZUi\ihN99G$o3+[& k/bkrftͱH;eReSӁߢ^$XA"W,8}9,5>}㼬oד Ȁo7?TX=4D "Ni͇'~zq`d㞟{B4zO7&LVŗu26k3a"m#[ԽMO1K!^!B-KB M6.crx:9<6˧VxI}W uû&jwk=O p[W{"ܮBȎ3 si_f|)9:]pF^k jb^YNXf(3YvsH)??!=XM6Njh0.1Qj(<FT*<#3`@'Uͳ>FP6J1JҊPFW/B?ցQQ@>z%-'樍FIЛAtMB`ƇIזfu È]# eSeָe{ 6<5DӎT{ FVrF(LhJO Gɿ*E\]M>@zCfb 1@0 *H  1 client0! *H  1Time 15955753665540 *H 00 *H 0) *H  0ȟ,D?sP`С'vN ZJfT( f=ӬiKۥN@gjQYEV޵ȇMPbXwv$8O(BY7d88?If["%k]Ew ;In5sx`~tœ 8lku|ncQW[&>D١Rmkx?:DLKRc/Pe ) b9=~U ]Q\)/#ZQ[ג 2i֔:M .f&D'.]%}Qch+ 2iC``$^,-F_~q˼HNE>%ˢ՗HJ}S6qum 9,RE}%f%0HK;t4g FC2:7lL}ٰzZ`փZ..8c]=6cw &jNܞߟj/\o&G$A7jzdġ!ni꼊/1z'P6y.ix*"RJ)eIlVIe{ Xj[{b9$X>K&n= rI*Nn7OmUyE=e`4;_b^yXbZ ӢlV| COq[r`O"G5X q\esT, <-yM]H./68eZno.쵀`F6XVymabk0wPS4,|y僔<ضs˄:_][~EoS)N'4>.<7 l?p:ackmxޥ[,6FtT[wChY(G&l<,#k9-+!nܴB/ӎL8KL-L805zMHEEͶSgjWb20w8j{߳ m@iCHE`rjgY0!0 +$ ݜJBтL$*t%clj-http-3.12.3/test-resources/keystore000066400000000000000000000066071407075231000201140ustar00rootroot000000000000000 0 < *H  - )0 %0i *H ZV0R0N *H  00) *H  0z%ͺd 1ڌD?hBPG_D-! va>cͽqtEpfEx푉xK},I]i$w`}slhh|8@dI8].>gU L>[Xsg|^8 [@gi\u2#l c!fC"-Y؊j.L:潖!bm<8@bI`f U'KdjHWncTǏnR)pcwSo:HtOZ/R;0WjOr X`j^4 %L_aKM^?*zUOE.2hyY%Xv+ {S.^Ks  ? sZ̞Asw0VJ%RэER<(+ξWD$ADY1@0 *H  1 server0! *H  1Time 15955751448210 *H 00 *H 0) *H  01$_us6GP`g`%!IMF{u̸,07M0^Ƅ{逦R̈i螋"lPQ@O]T^&ڙYU81[uN9^ήEk:cjF͔߆+ra!0({lWˉp W S:\Hx=^t[킲#qsoP*ɿDN a sGW2ȹߘS)N"a=<*f2w3zDimA΀?$Xj)m1$n@BS c+{]6`B1.9Hi6ڧqǦoqA5(y!6v4P$.c7Y\bv%Nօ=E,m~^_9'Nk&bJHU-Hܐ8ld؅\C7G˗* d(pFhVd))Lܒx\G"q+!r sFM6plbi'!fޮ(ws{|Z܃#iyPE @Ni>QƋ@;\ͼ;nIYs#s_,iޖE41.9QOsDأW/ms2Ug,Xܚ8Y775=8|T$ԡT'G:eN!;a:6dUo#gH-dM>dyD{m_!YRGz;V~}ÌSualմ_*)8ZoqbǘJ\Ιe#f₈}; ΅܂%٨ɀMv_Q՝d$>p. AbpTX~{-'p7馊S=7 EUaHRmz E)Bk#()|x&m5 w=fiz৉`uT֪)+)0ʫy#VEy@tg  l7y<pw4nb'%';x!a5iwVy m]jg6wR;ǮHZ0aA(x#gT'&&&(v!38ƿM᧥Fe.qԨX;bjO;'B[R:. soiHn78b.%מҧ)Hd^@cngN9D_HIj6vY΢"-i0.%{gaܚDL9#ʦOK;9qgL wq򊙑sFAD|98)LUy(فĆlp<,5iN9]8LeR]+ɋOr?#;-s SHmBI+͗+5¬&hx/C`2-$ϫ1Q2nOz*0>0!0 +RH}N-süHπ%[3"zN[v(Hclj-http-3.12.3/test-resources/m.txt000066400000000000000000000000231407075231000173030ustar00rootroot00000000000000this is some file. clj-http-3.12.3/test-resources/small.jpg000066400000000000000000000001531407075231000201240ustar00rootroot00000000000000C        ? clj-http-3.12.3/test/000077500000000000000000000000001407075231000143035ustar00rootroot00000000000000clj-http-3.12.3/test/clj_http/000077500000000000000000000000001407075231000161125ustar00rootroot00000000000000clj-http-3.12.3/test/clj_http/test/000077500000000000000000000000001407075231000170715ustar00rootroot00000000000000clj-http-3.12.3/test/clj_http/test/client_test.clj000066400000000000000000002250051407075231000221040ustar00rootroot00000000000000(ns clj-http.test.client-test (:require [cheshire.core :as json] [clj-http.client :as client] [clj-http.conn-mgr :as conn] [clj-http.test.core-test :refer [run-server]] [clj-http.util :as util] [clojure.java.io :refer [resource]] [clojure.string :as str] [clojure.test :refer :all] [cognitect.transit :as transit] [ring.middleware.nested-params :refer [parse-nested-keys]] [ring.util.codec :refer [form-decode-str]] [slingshot.slingshot :refer [try+]]) (:import java.io.ByteArrayInputStream java.net.UnknownHostException org.apache.http.HttpEntity org.apache.logging.log4j.LogManager)) (defonce logger (LogManager/getLogger "clj-http.test.client-test")) (def base-req {:scheme :http :server-name "localhost" :server-port 18080}) (defn request ([req] (client/request (merge base-req req))) ([req respond raise] (client/request (merge base-req req) respond raise))) (defn parse-form-params [s] (->> (str/split (form-decode-str s) #"&") (map #(str/split % #"=")) (map #(vector (map keyword (parse-nested-keys (first %))) (second %))) (reduce (fn [m [ks v]] (assoc-in m ks v)) {}))) (deftest ^:integration roundtrip (run-server) ;; roundtrip with scheme as a keyword (let [resp (request {:uri "/get" :method :get})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= "get" (:body resp)))) ;; roundtrip with scheme as a string (let [resp (request {:uri "/get" :method :get :scheme "http"})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= "get" (:body resp)))) (let [params {:a "1" :b "2"}] (doseq [[content-type read-fn] [[nil (comp parse-form-params slurp)] [:x-www-form-urlencoded (comp parse-form-params slurp)] [:edn (comp read-string slurp)] [:transit+json #(client/parse-transit % :json)] [:transit+msgpack #(client/parse-transit % :msgpack)]]] (let [resp (request {:uri "/post" :as :stream :method :post :content-type content-type :form-params params})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= params (read-fn (:body resp))) (str "failed with content-type [" content-type "]")))))) (deftest ^:integration roundtrip-async (run-server) ;; roundtrip with scheme as a keyword (let [resp (promise) exception (promise) _ (request {:uri "/get" :method :get :async? true} resp exception)] (is (= 200 (:status @resp))) (is (= "close" (get-in @resp [:headers "connection"]))) (is (= "get" (:body @resp))) (is (not (realized? exception)))) ;; roundtrip with scheme as a string (let [resp (promise) exception (promise) _ (request {:uri "/get" :method :get :scheme "http" :async? true} resp exception)] (is (= 200 (:status @resp))) (is (= "close" (get-in @resp [:headers "connection"]))) (is (= "get" (:body @resp))) (is (not (realized? exception)))) (let [params {:a "1" :b "2"}] (doseq [[content-type read-fn] [[nil (comp parse-form-params slurp)] [:x-www-form-urlencoded (comp parse-form-params slurp)] [:edn (comp read-string slurp)] [:transit+json #(client/parse-transit % :json)] [:transit+msgpack #(client/parse-transit % :msgpack)]]] (let [resp (promise) exception (promise) _ (request {:uri "/post" :as :stream :method :post :content-type content-type :flatten-nested-keys [] :form-params params :async? true} resp exception)] (is (= 200 (:status @resp))) (is (= "close" (get-in @resp [:headers "connection"]))) (is (= params (read-fn (:body @resp)))) (is (not (realized? exception))))))) (def ^:dynamic *test-dynamic-var* nil) (deftest ^:integration async-preserves-dynamic-variable-bindings (run-server) (let [expected-var "cat"] (binding [*test-dynamic-var* expected-var] (let [test-fn (fn [uri success-p fail-p] (request {:uri uri :method :get :scheme "http" :async? true} (fn [_] (deliver success-p *test-dynamic-var*) (deliver fail-p :success)) (fn [_] (deliver success-p :fail) (deliver fail-p *test-dynamic-var*))))] (testing "dynamic variables on success responses" (let [success-p (promise) fail-p (promise)] (test-fn "/get" success-p fail-p) (is (= @success-p expected-var *test-dynamic-var*)) (is (= @fail-p :success) "Verify that we went through the success path, not the failure"))) (testing "dynamic variables on fail responses" (let [success-p (promise) fail-p (promise)] (test-fn "/json-bad" success-p fail-p) (is (= @success-p :fail) "Verify that we went through the failure path, not the success") (is (= @fail-p expected-var *test-dynamic-var*)))))))) (deftest ^:integration multipart-async (run-server) (let [resp (promise) exception (promise) _ (request {:uri "/post" :method :post :async? true :multipart [{:name "title" :content "some-file"} {:name "Content/Type" :content "text/plain"} {:name "file" :content (clojure.java.io/file "test-resources/m.txt")}]} resp exception )] (is (= 200 (:status @resp))) (is (not (realized? exception))) #_(when (realized? exception) (prn @exception))) ;; Regression Testing https://github.com/dakrone/clj-http/issues/560 (testing "multipart uploads larger than 25kb" (let [resp (promise) exception (promise) ;; assumption: file > 5kb file (clojure.java.io/file "test-resources/big_array_json.json") _ (request {:uri "/post" :method :post :async? true :multipart [{:name "part-1" :content file} {:name "part-2" :content file} {:name "part-3" :content file} {:name "part-4" :content file} {:name "part-5" :content file}]} resp exception)] (is (= 200 (:status (deref resp 500 :failed)))) (is (not (realized? exception)))))) (deftest ^:integration nil-input (is (thrown-with-msg? Exception #"Host URL cannot be nil" (client/get nil))) (is (thrown-with-msg? Exception #"Host URL cannot be nil" (client/post nil))) (is (thrown-with-msg? Exception #"Host URL cannot be nil" (client/put nil))) (is (thrown-with-msg? Exception #"Host URL cannot be nil" (client/delete nil)))) (defn async-identity-client "A async client which simply respond the request" [request respond raise] (respond request)) (defn is-passed [middleware req] (let [client (middleware identity)] (is (= req (client req))))) (defn is-passed-async [middleware req] (let [client (middleware async-identity-client) resp (promise) exception (promise) _ (client req resp exception)] (is (= req @resp)) (is (not (realized? exception))))) (defn is-applied [middleware req-in req-out] (let [client (middleware identity)] (is (= req-out (client req-in))))) (defn is-applied-async [middleware req-in req-out] (let [client (middleware async-identity-client) resp (promise) exception (promise) _ (client req-in resp exception)] (is (= req-out @resp)) (is (not (realized? exception))))) (deftest redirect-on-get (let [client (fn [req] (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat"}} {:status 200 :req req})) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method :get})] (is (= 200 (:status resp))) (is (= :get (:request-method (:req resp)))) (is (= :http (:scheme (:req resp)))) (is (= ["http://example.com" "http://example.net/bat"] (:trace-redirects resp))) (is (= "/bat" (:uri (:req resp)))))) (deftest redirect-on-get-async (let [client (fn [req respond raise] (respond (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat"}} {:status 200 :req req}))) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method :get} resp exception)] (is (= 200 (:status @resp))) (is (= :get (:request-method (:req @resp)))) (is (= :http (:scheme (:req @resp)))) (is (= ["http://example.com" "http://example.net/bat"] (:trace-redirects @resp))) (is (= "/bat" (:uri (:req @resp)))) (is (not (realized? exception))))) (deftest relative-redirect-on-get (let [client (fn [req] (if (:redirects-count req) {:status 200 :req req} {:status 302 :headers {"location" "/bat"}})) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method :get})] (is (= 200 (:status resp))) (is (= :get (:request-method (:req resp)))) (is (= :http (:scheme (:req resp)))) (is (= ["http://example.com" "http://example.com/bat"] (:trace-redirects resp))) (is (= "/bat" (:uri (:req resp)))))) (deftest relative-redirect-on-get-async (let [client (fn [req respond raise] (respond (if (:redirects-count req) {:status 200 :req req} {:status 302 :headers {"location" "/bat"}}))) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method :get} resp exception)] (is (= 200 (:status @resp))) (is (= :get (:request-method (:req @resp)))) (is (= :http (:scheme (:req @resp)))) (is (= ["http://example.com" "http://example.com/bat"] (:trace-redirects @resp))) (is (= "/bat" (:uri (:req @resp)))) (is (not (realized? exception))))) (deftest trace-redirects-using-uri (let [client (fn [req] {:status 200 :req req}) r-client (-> client client/wrap-redirects) resp (r-client {:scheme :http :server-name "example.com" :uri "/" :request-method :get})] (is (= 200 (:status resp))) (is (= :get (:request-method (:req resp)))) (is (= :http (:scheme (:req resp)))) (is (= [] (:trace-redirects resp))))) (deftest trace-redirects-using-uri-async (let [client (fn [req respond raise] (respond {:status 200 :req req})) r-client (-> client client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:scheme :http :server-name "example.com" :uri "/" :request-method :get} resp exception)] (is (= 200 (:status @resp))) (is (= :get (:request-method (:req @resp)))) (is (= :http (:scheme (:req @resp)))) (is (= [] (:trace-redirects @resp))) (is (not (realized? exception))))) (deftest redirect-without-location-header (let [client (fn [req] {:status 302 :body "no redirection here"}) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method :get})] (is (= 302 (:status resp))) (is (= ["http://example.com"] (:trace-redirects resp))) (is (= "no redirection here" (:body resp))))) (deftest redirect-without-location-header-async (let [client (fn [req respond raise] (respond {:status 302 :body "no redirection here"})) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method :get} resp exception)] (is (= 302 (:status @resp))) (is (= ["http://example.com"] (:trace-redirects @resp))) (is (= "no redirection here" (:body @resp))) (is (not (realized? exception))))) (deftest redirect-with-query-string (let [client (fn [req] (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat?x=y"}} {:status 200 :req req})) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method :get :query-params {:x "z"}})] (is (= 200 (:status resp))) (is (= :get (:request-method (:req resp)))) (is (= :http (:scheme (:req resp)))) (is (= ["http://example.com" "http://example.net/bat?x=y"] (:trace-redirects resp))) (is (= "/bat" (:uri (:req resp)))) (is (= "x=y" (:query-string (:req resp)))) (is (nil? (:query-params (:req resp)))))) (deftest redirect-with-query-string-async (let [client (fn [req respond raise] (respond (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat?x=y"}} {:status 200 :req req}))) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method :get :query-params {:x "z"}} resp exception)] (is (= 200 (:status @resp))) (is (= :get (:request-method (:req @resp)))) (is (= :http (:scheme (:req @resp)))) (is (= ["http://example.com" "http://example.net/bat?x=y"] (:trace-redirects @resp))) (is (= "/bat" (:uri (:req @resp)))) (is (= "x=y" (:query-string (:req @resp)))) (is (nil? (:query-params (:req @resp)))) (is (not (realized? exception))))) (deftest max-redirects (let [client (fn [req] (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat"}} {:status 200 :req req})) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method :get :max-redirects 0})] (is (= 302 (:status resp))) (is (= ["http://example.com"] (:trace-redirects resp))) (is (= "http://example.net/bat" (get (:headers resp) "location"))))) (deftest max-redirects-async (let [client (fn [req respond raise] (respond (if (= "example.com" (:server-name req)) {:status 302 :headers {"location" "http://example.net/bat"}} {:status 200 :req req}))) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method :get :max-redirects 0} resp exception)] (is (= 302 (:status @resp))) (is (= ["http://example.com"] (:trace-redirects @resp))) (is (= "http://example.net/bat" (get (:headers @resp) "location"))) (is (not (realized? exception))))) (deftest redirect-303-to-get-on-any-method (doseq [method [:get :head :post :delete :put :option]] (let [client (fn [req] (if (= "example.com" (:server-name req)) {:status 303 :headers {"location" "http://example.net/bat"}} {:status 200 :req req})) r-client (-> client client/wrap-url client/wrap-redirects) resp (r-client {:server-name "example.com" :url "http://example.com" :request-method method})] (is (= 200 (:status resp))) (is (= :get (:request-method (:req resp)))) (is (= :http (:scheme (:req resp)))) (is (= ["http://example.com" "http://example.net/bat"] (:trace-redirects resp))) (is (= "/bat" (:uri (:req resp))))))) (deftest redirect-303-to-get-on-any-method-async (doseq [method [:get :head :post :delete :put :option]] (let [client (fn [req respond raise] (respond (if (= "example.com" (:server-name req)) {:status 303 :headers {"location" "http://example.net/bat"}} {:status 200 :req req}))) r-client (-> client client/wrap-url client/wrap-redirects) resp (promise) exception (promise) _ (r-client {:server-name "example.com" :url "http://example.com" :request-method method} resp exception)] (is (= 200 (:status @resp))) (is (= :get (:request-method (:req @resp)))) (is (= :http (:scheme (:req @resp)))) (is (= ["http://example.com" "http://example.net/bat"] (:trace-redirects @resp))) (is (= "/bat" (:uri (:req @resp)))) (is (not (realized? exception)))))) (deftest pass-on-non-redirect (let [client (fn [req] {:status 200 :body (:body req)}) r-client (client/wrap-redirects client) resp (r-client {:body "ok" :url "http://example.com"})] (is (= 200 (:status resp))) (is (= ["http://example.com"] (:trace-redirects resp))) (is (= "ok" (:body resp))))) (deftest pass-on-non-redirect-async (let [client (fn [req respond raise] (respond {:status 200 :body (:body req)})) r-client (client/wrap-redirects client) resp (promise) exception (promise) _ (r-client {:body "ok" :url "http://example.com"} resp exception)] (is (= 200 (:status @resp))) (is (= ["http://example.com"] (:trace-redirects @resp))) (is (= "ok" (:body @resp))) (is (not (realized? exception))))) (deftest pass-on-non-redirectable-methods (doseq [method [:put :post :delete] status [301 302 307 308]] (let [client (fn [req] {:status status :body (:body req) :headers {"location" "http://example.com/bat"}}) r-client (client/wrap-redirects client) resp (r-client {:body "ok" :url "http://example.com" :request-method method})] (is (= status (:status resp))) (is (= ["http://example.com"] (:trace-redirects resp))) (is (= {"location" "http://example.com/bat"} (:headers resp))) (is (= "ok" (:body resp)))))) (deftest pass-on-non-redirectable-methods-async (doseq [method [:put :post :delete] status [301 302 307 308]] (let [client (fn [req respond raise] (respond {:status status :body (:body req) :headers {"location" "http://example.com/bat"}})) r-client (client/wrap-redirects client) resp (promise) exception (promise) _ (r-client {:body "ok" :url "http://example.com" :request-method method} resp exception)] (is (= status (:status @resp))) (is (= ["http://example.com"] (:trace-redirects @resp))) (is (= {"location" "http://example.com/bat"} (:headers @resp))) (is (= "ok" (:body @resp))) (is (not (realized? exception)))))) (deftest force-redirects-on-non-redirectable-methods (doseq [method [:put :post :delete] [status expected-method] [[301 :get] [302 :get] [307 method]]] (let [client (fn [{:keys [trace-redirects body] :as req}] (if trace-redirects {:status 200 :body body :trace-redirects trace-redirects :req req} {:status status :body body :req req :headers {"location" "http://example.com/bat"}})) r-client (client/wrap-redirects client) resp (r-client {:body "ok" :url "http://example.com" :request-method method :force-redirects true})] (is (= 200 (:status resp))) (is (= ["http://example.com" "http://example.com/bat"] (:trace-redirects resp))) (is (= "ok" (:body resp))) (is (= expected-method (:request-method (:req resp))))))) (deftest force-redirects-on-non-redirectable-methods-async (doseq [method [:put :post :delete] [status expected-method] [[301 :get] [302 :get] [307 method]]] (let [client (fn [{:keys [trace-redirects body] :as req} respond raise] (respond (if trace-redirects {:status 200 :body body :trace-redirects trace-redirects :req req} {:status status :body body :req req :headers {"location" "http://example.com/bat"}}))) r-client (client/wrap-redirects client) resp (promise) exception (promise) _ (r-client {:body "ok" :url "http://example.com" :request-method method :force-redirects true} resp exception)] (is (= 200 (:status @resp))) (is (= ["http://example.com" "http://example.com/bat"] (:trace-redirects @resp))) (is (= "ok" (:body @resp))) (is (= expected-method (:request-method (:req @resp)))) (is (not (realized? exception)))))) (deftest pass-on-follow-redirects-false (let [client (fn [req] {:status 302 :body (:body req)}) r-client (client/wrap-redirects client) resp (r-client {:body "ok" :follow-redirects false})] (is (= 302 (:status resp))) (is (= "ok" (:body resp))) (is (nil? (:trace-redirects resp))))) (deftest pass-on-follow-redirects-false-async (let [client (fn [req respond raise] (respond {:status 302 :body (:body req)})) r-client (client/wrap-redirects client) resp (promise) exception (promise) _ (r-client {:body "ok" :follow-redirects false} resp exception)] (is (= 302 (:status @resp))) (is (= "ok" (:body @resp))) (is (nil? (:trace-redirects @resp))) (is (not (realized? exception))))) (deftest throw-on-exceptional (let [client (fn [req] {:status 500}) e-client (client/wrap-exceptions client)] (is (thrown-with-msg? Exception #"500" (e-client {})))) (let [client (fn [req] {:status 500 :body "foo"}) e-client (client/wrap-exceptions client)] (is (thrown-with-msg? Exception #":body" (e-client {:throw-entire-message? true}))))) (deftest throw-on-custom-exceptional (let [client (fn [req] {:status 201}) e-client (client/wrap-exceptions client)] (is (thrown-with-msg? Exception #"201" (e-client {:unexceptional-status #{200}}))))) (deftest throw-type-field (let [client (fn [req] {:status 500}) e-client (client/wrap-exceptions client)] (try+ (e-client {}) (catch [:type :clj-http.client/unexceptional-status] _ (is true)) (catch Object _ (is false ":type selector was not caught."))))) (deftest throw-on-exceptional-async (let [client (fn [req respond raise] (try (respond {:status 500}) (catch Throwable ex (raise ex)))) e-client (client/wrap-exceptions client) resp (promise) exception (promise) _ (e-client {} resp exception)] (is (thrown-with-msg? Exception #"500" (throw @exception)))) (let [client (fn [req respond raise] (try (respond {:status 500 :body "foo"}) (catch Throwable ex (raise ex)))) e-client (client/wrap-exceptions client) resp (promise) exception (promise) _ (e-client {:throw-entire-message? true} resp exception)] (is (thrown-with-msg? Exception #":body" (throw @exception))))) (deftest pass-on-non-exceptional (let [client (fn [req] {:status 200}) e-client (client/wrap-exceptions client) resp (e-client {})] (is (= 200 (:status resp))))) (deftest pass-on-custom-non-exceptional (let [client (fn [req] {:status 500}) e-client (client/wrap-exceptions client) resp (e-client {:unexceptional-status #{200 500}})] (is (= 500 (:status resp))))) (deftest pass-on-non-exceptional-async (let [client (fn [req respond raise] (respond {:status 200})) e-client (client/wrap-exceptions client) resp (promise) exception (promise) _ (e-client {} resp exception)] (is (= 200 (:status @resp))) (is (not (realized? exception))))) (deftest pass-on-exceptional-when-surpressed (let [client (fn [req] {:status 500}) e-client (client/wrap-exceptions client) resp (e-client {:throw-exceptions false})] (is (= 500 (:status resp))))) (deftest pass-on-exceptional-when-surpressed-async (let [client (fn [req respond raise] (respond {:status 500})) e-client (client/wrap-exceptions client) resp (promise) exception (promise) _ (e-client {:throw-exceptions false} resp exception)] (is (= 500 (:status @resp))) (is (not (realized? exception))))) (deftest apply-on-compressed (let [client (fn [req] (is (= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) {:body (util/gzip (util/utf8-bytes "foofoofoo")) :headers {"content-encoding" "gzip"}}) c-client (client/wrap-decompression client) resp (c-client {})] (is (= "foofoofoo" (util/utf8-string (:body resp)))) (is (= "gzip" (:orig-content-encoding resp))) (is (= nil (get-in resp [:headers "content-encoding"]))))) (deftest apply-on-compressed-async (let [client (fn [req respond raise] (is (= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) (respond {:body (util/gzip (util/utf8-bytes "foofoofoo")) :headers {"content-encoding" "gzip"}})) c-client (client/wrap-decompression client) resp (promise) exception (promise) _ (c-client {} resp exception)] (is (= "foofoofoo" (util/utf8-string (:body @resp)))) (is (= "gzip" (:orig-content-encoding @resp))) (is (= nil (get-in @resp [:headers "content-encoding"]))))) (deftest apply-on-deflated (let [client (fn [req] (is (= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) {:body (util/deflate (util/utf8-bytes "barbarbar")) :headers {"content-encoding" "deflate"}}) c-client (client/wrap-decompression client) resp (c-client {})] (is (= "barbarbar" (-> resp :body util/force-byte-array util/utf8-string)) "string correctly inflated") (is (= "deflate" (:orig-content-encoding resp))) (is (= nil (get-in resp [:headers "content-encoding"]))))) (deftest apply-on-deflated-async (let [client (fn [req respond raise] (is (= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) (respond {:body (util/deflate (util/utf8-bytes "barbarbar")) :headers {"content-encoding" "deflate"}})) c-client (client/wrap-decompression client) resp (promise) exception (promise) _ (c-client {} resp exception)] (is (= "barbarbar" (-> @resp :body util/force-byte-array util/utf8-string)) "string correctly inflated") (is (= "deflate" (:orig-content-encoding @resp))) (is (= nil (get-in @resp [:headers "content-encoding"]))))) (deftest t-disabled-body-decompression (let [client (fn [req] (is (not= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) {:body (util/deflate (util/utf8-bytes "barbarbar")) :headers {"content-encoding" "deflate"}}) c-client (client/wrap-decompression client) resp (c-client {:decompress-body false})] (is (= (slurp (util/inflate (util/deflate (util/utf8-bytes "barbarbar")))) (slurp (util/inflate (-> resp :body util/force-byte-array)))) "string not inflated") (is (= nil (:orig-content-encoding resp))) (is (= "deflate" (get-in resp [:headers "content-encoding"]))))) (deftest t-weird-non-known-compression (let [client (fn [req] (is (= "gzip, deflate" (get-in req [:headers "accept-encoding"]))) {:body (util/utf8-bytes "foofoofoo") :headers {"content-encoding" "pig-latin"}}) c-client (client/wrap-decompression client) resp (c-client {})] (is (= "foofoofoo" (util/utf8-string (:body resp)))) (is (= "pig-latin" (:orig-content-encoding resp))) (is (= "pig-latin" (get-in resp [:headers "content-encoding"]))))) (deftest pass-on-non-compressed (let [c-client (client/wrap-decompression (fn [req] {:body "foo"})) resp (c-client {:uri "/foo"})] (is (= "foo" (:body resp))))) (deftest apply-on-accept (is-applied client/wrap-accept {:accept :json} {:headers {"accept" "application/json"}}) (is-applied client/wrap-accept {:accept :transit+json} {:headers {"accept" "application/transit+json"}}) (is-applied client/wrap-accept {:accept :transit+msgpack} {:headers {"accept" "application/transit+msgpack"}})) (deftest apply-on-accept-async (is-applied-async client/wrap-accept {:accept :json} {:headers {"accept" "application/json"}}) (is-applied-async client/wrap-accept {:accept :transit+json} {:headers {"accept" "application/transit+json"}}) (is-applied-async client/wrap-accept {:accept :transit+msgpack} {:headers {"accept" "application/transit+msgpack"}})) (deftest pass-on-no-accept (is-passed client/wrap-accept {:uri "/foo"})) (deftest pass-on-no-accept-async (is-passed-async client/wrap-accept {:uri "/foo"})) (deftest apply-on-accept-encoding (is-applied client/wrap-accept-encoding {:accept-encoding [:identity :gzip]} {:headers {"accept-encoding" "identity, gzip"}})) (deftest apply-custom-accept-encoding (testing "no custom encodings to accept" (is-applied (comp client/wrap-accept-encoding client/wrap-decompression) {} {:headers {"accept-encoding" "gzip, deflate"} :orig-content-encoding nil})) (testing "accept some custom encodings, but still include gzip and deflate" (is-applied (comp client/wrap-accept-encoding client/wrap-decompression) {:accept-encoding [:foo :bar]} {:headers {"accept-encoding" "foo, bar, gzip, deflate"} :orig-content-encoding nil})) (testing "accept some custom encodings, but exclude gzip and deflate" (is-applied (comp client/wrap-accept-encoding client/wrap-decompression) {:accept-encoding [:foo :bar] :decompress-body false} {:headers {"accept-encoding" "foo, bar"} :decompress-body false}))) (deftest pass-on-no-accept-encoding (is-passed client/wrap-accept-encoding {:uri "/foo"})) (deftest apply-on-output-coercion (let [client (fn [req] {:body (util/utf8-bytes "foo")}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo"})] (is (= "foo" (:body resp))))) (deftest apply-on-output-coercion-async (let [client (fn [req respond raise] (respond {:body (util/utf8-bytes "foo")})) o-client (client/wrap-output-coercion client) resp (promise) exception (promise) _ (o-client {:uri "/foo"} resp exception)] (is (= "foo" (:body @resp))) (is (not (realized? exception))))) (deftest pass-on-no-output-coercion (let [client (fn [req] {:body nil}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo"})] (is (nil? (:body resp)))) (let [the-stream (ByteArrayInputStream. (byte-array [])) client (fn [req] {:body the-stream}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo" :as :stream})] (is (= the-stream (:body resp)))) (let [client (fn [req] {:body :thebytes}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo" :as :byte-array})] (is (= :thebytes (:body resp))))) (deftest pass-on-no-output-coercion-async (let [client (fn [req] {:body nil}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo"})] (is (nil? (:body resp)))) (let [the-stream (ByteArrayInputStream. (byte-array [])) client (fn [req] {:body the-stream}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo" :as :stream})] (is (= the-stream (:body resp)))) (let [client (fn [req] {:body :thebytes}) o-client (client/wrap-output-coercion client) resp (o-client {:uri "/foo" :as :byte-array})] (is (= :thebytes (:body resp))))) (deftest apply-on-input-coercion (let [i-client (client/wrap-input-coercion identity) resp (i-client {:body "foo"}) resp2 (i-client {:body "foo2" :body-encoding "ASCII"}) data (slurp (.getContent ^HttpEntity (:body resp))) data2 (slurp (.getContent ^HttpEntity (:body resp2)))] (is (= "UTF-8" (:character-encoding resp))) (is (= "foo" data)) (is (= "ASCII" (:character-encoding resp2))) (is (= "foo2" data2)))) (deftest apply-on-input-coercion-async (let [i-client (client/wrap-input-coercion (fn [request respond raise] (respond request))) resp (promise) _ (i-client {:body "foo"} resp nil) resp2 (promise) _ (i-client {:body "foo2" :body-encoding "ASCII"} resp2 nil) data (slurp (.getContent ^HttpEntity (:body @resp))) data2 (slurp (.getContent ^HttpEntity (:body @resp2)))] (is (= "UTF-8" (:character-encoding @resp))) (is (= "foo" data)) (is (= "ASCII" (:character-encoding @resp2))) (is (= "foo2" data2)))) (deftest pass-on-no-input-coercion (is-passed client/wrap-input-coercion {:body nil})) (deftest pass-on-no-input-coercion (is-passed-async client/wrap-input-coercion {:body nil})) (deftest no-length-for-input-stream (let [i-client (client/wrap-input-coercion identity) resp1 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo"))}) resp2 (i-client {:body (ByteArrayInputStream. (util/utf8-bytes "foo")) :length 3}) ^HttpEntity body1 (:body resp1) ^HttpEntity body2 (:body resp2)] (is (= -1 (.getContentLength body1))) (is (= 3 (.getContentLength body2))))) (deftest apply-on-content-type (is-applied client/wrap-content-type {:content-type :json} {:headers {"content-type" "application/json"} :content-type :json}) (is-applied client/wrap-content-type {:content-type :json :character-encoding "UTF-8"} {:headers {"content-type" "application/json; charset=UTF-8"} :content-type :json :character-encoding "UTF-8"}) (is-applied client/wrap-content-type {:content-type :transit+json} {:headers {"content-type" "application/transit+json"} :content-type :transit+json}) (is-applied client/wrap-content-type {:content-type :transit+msgpack} {:headers {"content-type" "application/transit+msgpack"} :content-type :transit+msgpack})) (deftest apply-on-content-type-async (is-applied-async client/wrap-content-type {:content-type :json} {:headers {"content-type" "application/json"} :content-type :json}) (is-applied-async client/wrap-content-type {:content-type :json :character-encoding "UTF-8"} {:headers {"content-type" "application/json; charset=UTF-8"} :content-type :json :character-encoding "UTF-8"}) (is-applied-async client/wrap-content-type {:content-type :transit+json} {:headers {"content-type" "application/transit+json"} :content-type :transit+json}) (is-applied-async client/wrap-content-type {:content-type :transit+msgpack} {:headers {"content-type" "application/transit+msgpack"} :content-type :transit+msgpack})) (deftest pass-on-no-content-type (is-passed client/wrap-content-type {:uri "/foo"})) (deftest apply-on-query-params (is-applied client/wrap-query-params {:query-params {"foo" "bar" "dir" "<<"}} {:query-string "foo=bar&dir=%3C%3C"}) (is-applied client/wrap-query-params {:query-string "foo=1" :query-params {"foo" ["2" "3"]}} {:query-string "foo=1&foo=2&foo=3"})) (deftest apply-on-query-params-async (is-applied-async client/wrap-query-params {:query-params {"foo" "bar" "dir" "<<"}} {:query-string "foo=bar&dir=%3C%3C"}) (is-applied-async client/wrap-query-params {:query-string "foo=1" :query-params {"foo" ["2" "3"]}} {:query-string "foo=1&foo=2&foo=3"})) (deftest pass-on-no-query-params (is-passed client/wrap-query-params {:uri "/foo"})) (deftest apply-on-basic-auth (is-applied client/wrap-basic-auth {:basic-auth ["Aladdin" "open sesame"]} {:headers {"authorization" "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}})) (deftest apply-on-basic-auth-async (is-applied-async client/wrap-basic-auth {:basic-auth ["Aladdin" "open sesame"]} {:headers {"authorization" "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}})) (deftest pass-on-no-basic-auth (is-passed client/wrap-basic-auth {:uri "/foo"})) (deftest apply-on-oauth (is-applied client/wrap-oauth {:oauth-token "my-token"} {:headers {"authorization" "Bearer my-token"}})) (deftest apply-on-oauth-async (is-applied-async client/wrap-oauth {:oauth-token "my-token"} {:headers {"authorization" "Bearer my-token"}})) (deftest pass-on-no-oauth (is-passed client/wrap-oauth {:uri "/foo"})) (deftest apply-on-method (let [m-client (client/wrap-method identity) echo (m-client {:key :val :method :post})] (is (= :val (:key echo))) (is (= :post (:request-method echo))) (is (not (:method echo))))) (deftest apply-on-method-async (let [m-client (client/wrap-method async-identity-client) echo (promise) exception (promise) _ (m-client {:key :val :method :post} echo exception)] (is (= :val (:key @echo))) (is (= :post (:request-method @echo))) (is (not (:method @echo))))) (deftest pass-on-no-method (let [m-client (client/wrap-method identity) echo (m-client {:key :val})] (is (= :val (:key echo))) (is (not (:request-method echo))))) (deftest apply-on-url (let [u-client (client/wrap-url identity) resp (u-client {:url "http://google.com:8080/baz foo?bar=bat bit?"})] (is (= :http (:scheme resp))) (is (= "google.com" (:server-name resp))) (is (= 8080 (:server-port resp))) (is (= "/baz%20foo" (:uri resp))) (is (= "bar=bat%20bit?" (:query-string resp))))) (deftest apply-on-url (let [u-client (client/wrap-url async-identity-client) resp (promise) exception (promise) _ (u-client {:url "http://google.com:8080/baz foo?bar=bat bit?"} resp exception)] (is (= :http (:scheme @resp))) (is (= "google.com" (:server-name @resp))) (is (= 8080 (:server-port @resp))) (is (= "/baz%20foo" (:uri @resp))) (is (= "bar=bat%20bit?" (:query-string @resp))) (is (not (realized? exception))))) (deftest pass-on-no-url (let [u-client (client/wrap-url identity) resp (u-client {:uri "/foo"})] (is (= "/foo" (:uri resp))))) (deftest provide-default-port (is (= nil (-> "http://example.com/" client/parse-url :server-port))) (is (= 8080 (-> "http://example.com:8080/" client/parse-url :server-port))) (is (= nil (-> "https://example.com/" client/parse-url :server-port))) (is (= 8443 (-> "https://example.com:8443/" client/parse-url :server-port))) (is (= "https://example.com:8443/" (-> "https://example.com:8443/" client/parse-url :url)))) (deftest decode-credentials-from-url (is (= "fred's diner:fred's password" (-> "http://fred%27s%20diner:fred%27s%20password@example.com/foo" client/parse-url :user-info)))) (deftest unparse-url (is (= "http://fred's diner:fred's password@example.com/foo" (-> "http://fred%27s%20diner:fred%27s%20password@example.com/foo" client/parse-url client/unparse-url))) (is (= "https://foo:bar@example.org:8080" (-> "https://foo:bar@example.org:8080" client/parse-url client/unparse-url))) (is (= "ftp://example.org?foo" (-> "ftp://example.org?foo" client/parse-url client/unparse-url)))) (defrecord Point [x y]) (def write-point "Write a point in Transit format." (transit/write-handler (constantly "point") (fn [point] [(:x point) (:y point)]) (constantly nil))) (def read-point "Read a point in Transit format." (transit/read-handler (fn [[x y]] (->Point x y)))) (def transit-opts "Transit read and write options." {:encode {:handlers {Point write-point}} :decode {:handlers {"point" read-point}}}) (def transit-opts-deprecated "Deprecated Transit read and write options." {:handlers {Point write-point "point" read-point}}) (deftest apply-on-form-params (testing "With form params" (let [param-client (client/wrap-form-params identity) resp (param-client {:request-method :post :form-params (sorted-map :param1 "value1" :param2 "value2")})] (is (= "param1=value1¶m2=value2" (:body resp))) (is (= "application/x-www-form-urlencoded" (:content-type resp))) (is (not (contains? resp :form-params)))) (let [param-client (client/wrap-form-params identity) resp (param-client {:request-method :put :form-params (sorted-map :param1 "value1" :param2 "value2")})] (is (= "param1=value1¶m2=value2" (:body resp))) (is (= "application/x-www-form-urlencoded" (:content-type resp))) (is (not (contains? resp :form-params))))) (testing "With json form params" (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 "value2"} resp (param-client {:request-method :post :content-type :json :form-params params})] (is (= (json/encode params) (:body resp))) (is (= "application/json" (:content-type resp))) (is (not (contains? resp :form-params)))) (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 "value2"} resp (param-client {:request-method :put :content-type :json :form-params params})] (is (= (json/encode params) (:body resp))) (is (= "application/json" (:content-type resp))) (is (not (contains? resp :form-params)))) (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 "value2"} resp (param-client {:request-method :patch :content-type :json :form-params params})] (is (= (json/encode params) (:body resp))) (is (= "application/json" (:content-type resp))) (is (not (contains? resp :form-params)))) (let [param-client (client/wrap-form-params identity) params {:param1 (java.util.Date. (long 0))} resp (param-client {:request-method :put :content-type :json :form-params params :json-opts {:date-format "yyyy-MM-dd"}})] (is (= (json/encode params {:date-format "yyyy-MM-dd"}) (:body resp))) (is (= "application/json" (:content-type resp))) (is (not (contains? resp :form-params))))) (testing "With EDN form params" (doseq [method [:post :put :patch]] (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 (Point. 1 2)} resp (param-client {:request-method method :content-type :edn :form-params params})] (is (= (pr-str params) (:body resp))) (is (= "application/edn" (:content-type resp))) (is (not (contains? resp :form-params)))))) (testing "With Transit/JSON form params" (doseq [method [:post :put :patch]] (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 (Point. 1 2)} resp (param-client {:request-method method :content-type :transit+json :form-params params :transit-opts transit-opts})] (is (= params (client/parse-transit (ByteArrayInputStream. (:body resp)) :json transit-opts))) (is (= "application/transit+json" (:content-type resp))) (is (not (contains? resp :form-params)))))) (testing "With Transit/MessagePack form params" (doseq [method [:post :put :patch]] (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 "value2"} resp (param-client {:request-method method :content-type :transit+msgpack :form-params params :transit-opts transit-opts})] (is (= params (client/parse-transit (ByteArrayInputStream. (:body resp)) :msgpack transit-opts))) (is (= "application/transit+msgpack" (:content-type resp))) (is (not (contains? resp :form-params)))))) (testing "With Transit/JSON form params and deprecated options" (let [param-client (client/wrap-form-params identity) params {:param1 "value1" :param2 (Point. 1 2)} resp (param-client {:request-method :post :content-type :transit+json :form-params params :transit-opts transit-opts-deprecated})] (is (= params (client/parse-transit (ByteArrayInputStream. (:body resp)) :json transit-opts-deprecated))) (is (= "application/transit+json" (:content-type resp))) (is (not (contains? resp :form-params))))) (testing "Ensure it does not affect GET requests" (let [param-client (client/wrap-form-params identity) resp (param-client {:request-method :get :body "untouched" :form-params {:param1 "value1" :param2 "value2"}})] (is (= "untouched" (:body resp))) (is (not (contains? resp :content-type))))) (testing "with no form params" (let [param-client (client/wrap-form-params identity) resp (param-client {:body "untouched"})] (is (= "untouched" (:body resp))) (is (not (contains? resp :content-type)))))) (deftest apply-on-form-params-async (testing "With form params" (let [param-client (client/wrap-form-params async-identity-client) resp (promise) exception (promise) _ (param-client {:request-method :post :form-params (sorted-map :param1 "value1" :param2 "value2")} resp exception)] (is (= "param1=value1¶m2=value2" (:body @resp))) (is (= "application/x-www-form-urlencoded" (:content-type @resp))) (is (not (contains? @resp :form-params))) (is (not (realized? exception)))) (let [param-client (client/wrap-form-params async-identity-client) resp (promise) exception (promise) _ (param-client {:request-method :put :form-params (sorted-map :param1 "value1" :param2 "value2")} resp exception)] (is (= "param1=value1¶m2=value2" (:body @resp))) (is (= "application/x-www-form-urlencoded" (:content-type @resp))) (is (not (contains? @resp :form-params))) (is (not (realized? exception))))) (testing "Ensure it does not affect GET requests" (let [param-client (client/wrap-form-params async-identity-client) resp (promise) exception (promise) _ (param-client {:request-method :get :body "untouched" :form-params {:param1 "value1" :param2 "value2"}} resp exception)] (is (= "untouched" (:body @resp))) (is (not (contains? @resp :content-type))) (is (not (realized? exception))))) (testing "with no form params" (let [param-client (client/wrap-form-params async-identity-client) resp (promise) exception (promise) _ (param-client {:body "untouched"} resp exception)] (is (= "untouched" (:body @resp))) (is (not (contains? @resp :content-type))) (is (not (realized? exception)))))) (deftest apply-on-nested-params (testing "nested parameter maps" (is-applied (comp client/wrap-form-params client/wrap-nested-params) {:query-params {"foo" "bar"} :form-params {"foo" "bar"} :flatten-nested-keys [:query-params :form-params]} {:query-params {"foo" "bar"} :form-params {"foo" "bar"} :flatten-nested-keys [:query-params :form-params]}) (is-applied (comp client/wrap-form-params client/wrap-nested-params) {:query-params {"x" {"y" "z"}} :form-params {"x" {"y" "z"}} :flatten-nested-keys [:query-params]} {:query-params {"x[y]" "z"} :form-params {"x" {"y" "z"}} :flatten-nested-keys [:query-params]}) (is-applied (comp client/wrap-form-params client/wrap-nested-params) {:query-params {"a" {"b" {"c" "d"}}} :form-params {"a" {"b" {"c" "d"}}} :flatten-nested-keys [:form-params]} {:query-params {"a" {"b" {"c" "d"}}} :form-params {"a[b][c]" "d"} :flatten-nested-keys [:form-params]}) (is-applied (comp client/wrap-form-params client/wrap-nested-params) {:query-params {"a" {"b" {"c" "d"}}} :form-params {"a" {"b" {"c" "d"}}} :flatten-nested-keys [:query-params :form-params]} {:query-params {"a[b][c]" "d"} :form-params {"a[b][c]" "d"} :flatten-nested-keys [:query-params :form-params]})) (testing "not creating empty param maps" (is-applied client/wrap-query-params {} {}))) (deftest t-ignore-unknown-host (is (thrown? UnknownHostException (client/get "http://example.invalid"))) (is (nil? (client/get "http://example.invalid" {:ignore-unknown-host? true})))) (deftest t-ignore-unknown-host-async (let [resp (promise) exception (promise)] (client/get "http://example.invalid" {:async? true} resp exception) (is (thrown? UnknownHostException (throw @exception)))) (let [resp (promise) exception (promise)] (client/get "http://example.invalid" {:ignore-unknown-host? true :async? true} resp exception) (is (nil? @resp)))) (deftest test-status-predicates (testing "2xx statuses" (doseq [s (range 200 299)] (is (client/success? {:status s})) (is (not (client/redirect? {:status s}))) (is (not (client/client-error? {:status s}))) (is (not (client/server-error? {:status s}))))) (testing "3xx statuses" (doseq [s (range 300 399)] (is (not (client/success? {:status s}))) (is (client/redirect? {:status s})) (is (not (client/client-error? {:status s}))) (is (not (client/server-error? {:status s}))))) (testing "4xx statuses" (doseq [s (range 400 499)] (is (not (client/success? {:status s}))) (is (not (client/redirect? {:status s}))) (is (client/client-error? {:status s})) (is (not (client/server-error? {:status s}))))) (testing "5xx statuses" (doseq [s (range 500 599)] (is (not (client/success? {:status s}))) (is (not (client/redirect? {:status s}))) (is (not (client/client-error? {:status s}))) (is (client/server-error? {:status s})))) (testing "409 Conflict" (is (client/conflict? {:status 409})) (is (not (client/conflict? {:status 201}))) (is (not (client/conflict? {:status 404}))))) (deftest test-wrap-lower-case-headers (is (= {:status 404} ((client/wrap-lower-case-headers (fn [r] r)) {:status 404}))) (is (= {:headers {"content-type" "application/json"}} ((client/wrap-lower-case-headers #(do (is (= {:headers {"accept" "application/json"}} %1)) {:headers {"Content-Type" "application/json"}})) {:headers {"Accept" "application/json"}})))) (deftest t-request-timing (is (pos? (:request-time ((client/wrap-request-timing (fn [r] (Thread/sleep 15) r)) {}))))) (deftest t-wrap-additional-header-parsing (let [^String text (slurp (resource "header-test.html")) client (fn [req] {:body (.getBytes text)}) new-client (client/wrap-additional-header-parsing client) resp (new-client {:decode-body-headers true}) resp2 (new-client {:decode-body-headers false}) resp3 ((client/wrap-additional-header-parsing (fn [req] {:body nil})) {:decode-body-headers true}) resp4 ((client/wrap-additional-header-parsing (fn [req] {:headers {"content-type" "application/pdf"} :body (.getBytes text)})) {:decode-body-headers true})] (is (= {"content-type" "text/html; charset=Shift_JIS" "content-style-type" "text/css" "content-script-type" "text/javascript"} (:headers resp))) (is (nil? (:headers resp2))) (is (nil? (:headers resp3))) (is (= {"content-type" "application/pdf"} (:headers resp4))))) (deftest t-wrap-additional-header-parsing-html5 (let [^String text (slurp (resource "header-html5-test.html")) client (fn [req] {:body (.getBytes text)}) new-client (client/wrap-additional-header-parsing client) resp (new-client {:decode-body-headers true})] (is (= {"content-type" "text/html; charset=UTF-8"} (:headers resp))))) (deftest ^:integration t-request-without-url-set (run-server) ;; roundtrip with scheme as a keyword (let [resp (request {:uri "/redirect-to-get" :method :get})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= "get" (:body resp))))) (deftest ^:integration t-reusable-conn-mgrs (run-server) (let [cm (conn/make-reusable-conn-manager {:timeout 10 :insecure? false}) resp1 (request {:uri "/redirect-to-get" :method :get :connection-manager cm}) resp2 (request {:uri "/redirect-to-get" :method :get})] (is (= 200 (:status resp1) (:status resp2))) (is (nil? (get-in resp1 [:headers "connection"])) "connection should remain open") (is (= "close" (get-in resp2 [:headers "connection"])) "connection should be closed") (.shutdown cm))) (deftest ^:integration t-reusable-async-conn-mgrs (run-server) (let [cm (conn/make-reuseable-async-conn-manager {:timeout 10 :insecure? false}) resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise)] (request {:async? true :uri "/redirect-to-get" :method :get :connection-manager cm} resp1 exce1) (request {:async? true :uri "/redirect-to-get" :method :get} resp2 exce2) (is (= 200 (:status @resp1) (:status @resp2))) (is (nil? (get-in @resp1 [:headers "connection"])) "connection should remain open") (is (= "close" (get-in @resp2 [:headers "connection"])) "connection should be closed") (is (not (realized? exce2))) (is (not (realized? exce1))) (.shutdown cm))) (deftest ^:integration t-with-async-pool (run-server) (client/with-async-connection-pool {} (let [resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise)] (request {:async? true :uri "/get" :method :get} resp1 exce1) (request {:async? true :uri "/get" :method :get} resp2 exce2) (is (= 200 (:status @resp1) (:status @resp2))) (is (not (realized? exce2))) (is (not (realized? exce1)))))) (deftest ^:integration t-with-async-pool-sleep (run-server) (client/with-async-connection-pool {} (let [resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise)] (request {:async? true :uri "/get" :method :get} resp1 exce1) (Thread/sleep 500) (request {:async? true :uri "/get" :method :get} resp2 exce2) (is (= 200 (:status @resp1) (:status @resp2))) (is (not (realized? exce2))) (is (not (realized? exce1)))))) (deftest ^:integration t-async-pool-wrap-exception (run-server) (client/with-async-connection-pool {} (let [resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise) count (atom 2)] (request {:async? true :uri "/error" :method :get} resp1 exce1) (Thread/sleep 500) (request {:async? true :uri "/get" :method :get} resp2 exce2) (is (realized? exce1)) (is (not (realized? exce2))) (is (= 200 (:status @resp2)))))) (deftest ^:integration t-async-pool-exception-when-start (run-server) (client/with-async-connection-pool {} (let [resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise) middleware (fn [client] (fn [req resp raise] (throw (Exception.))))] (client/with-additional-middleware [middleware] (try (request {:async? true :uri "/error" :method :get} resp1 exce1) (catch Throwable ex)) (Thread/sleep 500) (try (request {:async? true :uri "/get" :method :get} resp2 exce2) (catch Throwable ex)) (is (not (realized? exce1))) (is (not (realized? exce2))) (is (not (realized? resp1))) (is (not (realized? resp2))))))) (deftest ^:integration t-reuse-async-pool (run-server) (client/with-async-connection-pool {} (let [resp1 (promise) resp2 (promise) exce1 (promise) exce2 (promise)] (request {:async? true :uri "/get" :method :get} (fn [resp] (resp1 resp) (request (client/reuse-pool {:async? true :uri "/get" :method :get} resp) resp2 exce2)) exce1) (is (= 200 (:status @resp1) (:status @resp2))) (is (not (realized? exce2))) (is (not (realized? exce1)))))) (deftest ^:integration t-async-pool-redirect-to-get (run-server) (client/with-async-connection-pool {} (let [resp (promise) exce (promise)] (request {:async? true :uri "/redirect-to-get" :method :get :redirect-strategy :default} resp exce) (is (= 200 (:status @resp))) (is (not (realized? exce)))))) (deftest ^:integration t-async-pool-max-redirect (run-server) (client/with-async-connection-pool {} (let [resp (promise) exce (promise)] (request {:async? true :uri "/redirect" :method :get :redirect-strategy :default :throw-exceptions true} resp exce) (is @exce) (is (not (realized? resp)))))) (deftest test-url-encode-path (is (= (client/url-encode-illegal-characters "?foo bar+baz[]75") "?foo%20bar+baz%5B%5D75")) (is (= {:uri (str "/:@-._~!$&'()*+,=" ";" ":@-._~!$&'()*+," "=" ":@-._~!$&'()*+,==") :query-string (str "/?:@-._~!$'()*+,;" "=" "/?:@-._~!$'()*+,;==")} ;; This URL sucks, yes, it's actually a valid URL (select-keys (client/parse-url (str "http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+" ",=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'(" ")*+,;==#/?:@-._~!$&'()*+,;=")) [:uri :query-string]))) (let [all-chars (apply str (map char (range 256))) all-legal (client/url-encode-illegal-characters all-chars)] (is (= all-legal (client/url-encode-illegal-characters all-legal))))) (defmethod client/coerce-response-body :json+ms949 [req resp] (client/coerce-json-body req resp true "MS949")) (deftest t-coercion-methods (let [json-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}")) json-ms949-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"안뇽\"}" "MS949")) auto-body (ByteArrayInputStream. (.getBytes "{\"foo\":\"bar\"}")) edn-body (ByteArrayInputStream. (.getBytes "{:foo \"bar\"}")) transit-json-body (ByteArrayInputStream. (.getBytes "[\"^ \",\"~:foo\",\"bar\"]")) transit-msgpack-body (->> (map byte [-127 -91 126 58 102 111 111 -93 98 97 114]) (byte-array 11) (ByteArrayInputStream.)) www-form-urlencoded-body (ByteArrayInputStream. (.getBytes "foo=bar")) auto-www-form-urlencoded-body (ByteArrayInputStream. (.getBytes "foo=bar")) json-resp {:body json-body :status 200 :headers {"content-type" "application/json"}} json-ms949-resp {:body json-ms949-body :status 200 :headers {"content-type" "application/json; charset=ms949"}} auto-resp {:body auto-body :status 200 :headers {"content-type" "application/json"}} edn-resp {:body edn-body :status 200 :headers {"content-type" "application/edn"}} transit-json-resp {:body transit-json-body :status 200 :headers {"content-type" "application/transit-json"}} transit-msgpack-resp {:body transit-msgpack-body :status 200 :headers {"content-type" "application/transit-msgpack"}} www-form-urlencoded-resp {:body www-form-urlencoded-body :status 200 :headers {"content-type" "application/x-www-form-urlencoded"}} auto-www-form-urlencoded-resp {:body auto-www-form-urlencoded-body :status 200 :headers {"content-type" "application/x-www-form-urlencoded"}}] (is (= {:foo "bar"} (:body (client/coerce-response-body {:as :json} json-resp)) (:body (client/coerce-response-body {:as :clojure} edn-resp)) (:body (client/coerce-response-body {:as :auto} auto-resp)) (:body (client/coerce-response-body {:as :transit+json} transit-json-resp)) (:body (client/coerce-response-body {:as :transit+msgpack} transit-msgpack-resp)) (:body (client/coerce-response-body {:as :auto} auto-www-form-urlencoded-resp)) (:body (client/coerce-response-body {:as :x-www-form-urlencoded} www-form-urlencoded-resp)))) (is (= {:foo "안뇽"} (:body (client/coerce-response-body {:as :json+ms949} json-ms949-resp)))) (testing "throws AssertionError when optional libraries are not loaded" (with-redefs [client/json-enabled? false] (is (thrown? AssertionError (client/coerce-response-body {:as :json} json-resp))) (is (thrown? AssertionError (client/coerce-response-body {:as :auto} json-resp)))) (with-redefs [client/transit-enabled? false] (is (thrown? AssertionError (client/coerce-response-body {:as :transit+json} transit-json-resp))) (is (thrown? AssertionError (client/coerce-response-body {:as :transit+msgpack} transit-msgpack-resp)))) (with-redefs [client/ring-codec-enabled? false] (is (thrown? AssertionError (client/coerce-response-body {:as :x-www-form-urlencoded} www-form-urlencoded-resp))) (is (thrown? AssertionError (client/coerce-response-body {:as :auto} auto-www-form-urlencoded-resp))))))) (deftest t-reader-coercion (let [read-lines (fn [reader] (vec (take-while not-empty (repeatedly #(.readLine reader))))) reader-body (ByteArrayInputStream. (.getBytes "foo\nbar\n")) reader-resp {:body reader-body :status 200 :headers {"content-type" "text/plain; charset=utf-8"}} encoded-body (ByteArrayInputStream. (byte-array [0xA9])) encoded-resp {:body encoded-body :status 200 :headers {"content-type" "text/plain; charset=iso-8859-1"}} utf8-body (ByteArrayInputStream. (byte-array [0xC2 0xA9])) utf8-resp {:body utf8-body :status 200 :headers {"content-type" "text/plain; charset=utf-8"}}] (is (= ["foo" "bar"] (read-lines (:body (client/coerce-response-body {:as :reader} reader-resp))))) (is (= "©" (.readLine (:body (client/coerce-response-body {:as :reader} encoded-resp))) (.readLine (:body (client/coerce-response-body {:as :reader} utf8-resp))))))) (deftest ^:integration t-with-middleware (run-server) (is (:request-time (request {:uri "/get" :method :get}))) (is (= client/*current-middleware* client/default-middleware)) (client/with-middleware [client/wrap-url client/wrap-method #'client/wrap-request-timing] (is (:request-time (request {:uri "/get" :method :get}))) (is (= client/*current-middleware* [client/wrap-url client/wrap-method #'client/wrap-request-timing]))) (client/with-middleware (->> client/default-middleware (remove #{client/wrap-request-timing})) (is (not (:request-time (request {:uri "/get" :method :get})))) (is (not (contains? (set client/*current-middleware*) client/wrap-request-timing))) (is (contains? (set client/default-middleware) client/wrap-request-timing)))) (deftest t-detect-charset-by-content-type (is (= "UTF-8" (client/detect-charset nil))) (is (= "UTF-8"(client/detect-charset "application/json"))) (is (= "UTF-8"(client/detect-charset "text/html"))) (is (= "GBK"(client/detect-charset "application/json; charset=GBK"))) (is (= "ISO-8859-1" (client/detect-charset "application/json; charset=ISO-8859-1"))) (is (= "ISO-8859-1" (client/detect-charset "application/json; charset = ISO-8859-1"))) (is (= "GB2312" (client/detect-charset "text/html; Charset=GB2312")))) (deftest ^:integration customMethodTest (run-server) (let [resp (request {:uri "/propfind" :method "PROPFIND"})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= "propfind" (:body resp)))) (let [resp (request {:uri "/propfind-with-body" :method "PROPFIND" :body "propfindbody"})] (is (= 200 (:status resp))) (is (= "close" (get-in resp [:headers "connection"]))) (is (= "propfindbody" (:body resp))))) (deftest ^:integration status-line-parsing (run-server) (let [resp (request {:uri "/get" :method :get}) protocol-version (:protocol-version resp)] (is (= 200 (:status resp))) (is (= "HTTP" (:name protocol-version))) (is (= 1 (:major protocol-version))) (is (= 1 (:minor protocol-version))) (is (= "OK" (:reason-phrase resp))))) (deftest ^:integration multi-valued-query-params (run-server) (testing "default (repeating) multi-valued query params" (let [resp (request {:uri "/query-string" :method :get :query-params {:a [1 2 3] :b ["x" "y" "z"]}}) query-string (-> resp :body form-decode-str)] (is (= 200 (:status resp))) (is (.contains query-string "a=1&a=2&a=3") query-string) (is (.contains query-string "b=x&b=y&b=z") query-string))) (testing "multi-valued query params in indexed-style" (let [resp (request {:uri "/query-string" :method :get :multi-param-style :indexed :query-params {:a [1 2 3] :b ["x" "y" "z"]}}) query-string (-> resp :body form-decode-str)] (is (= 200 (:status resp))) (is (.contains query-string "a[0]=1&a[1]=2&a[2]=3") query-string) (is (.contains query-string "b[0]=x&b[1]=y&b[2]=z") query-string))) (testing "multi-valued query params in array-style" (let [resp (request {:uri "/query-string" :method :get :multi-param-style :array :query-params {:a [1 2 3] :b ["x" "y" "z"]}}) query-string (-> resp :body form-decode-str)] (is (= 200 (:status resp))) (is (.contains query-string "a[]=1&a[]=2&a[]=3") query-string) (is (.contains query-string "b[]=x&b[]=y&b[]=z") query-string))) (testing "multi-valued query params in comma-separated" (let [resp (request {:uri "/query-string" :method :get :multi-param-style :comma-separated :query-params {:a [1 2 3] :b ["x" "y" "z"]}}) query-string (-> resp :body form-decode-str)] (is (= 200 (:status resp))) (is (.contains query-string "a=1,2,3") query-string) (is (.contains query-string "b=x,y,z") query-string)))) (deftest t-wrap-flatten-nested-params (is-applied client/wrap-flatten-nested-params {} {:flatten-nested-keys [:query-params]}) (is-applied client/wrap-flatten-nested-params {:flatten-nested-keys []} {:flatten-nested-keys []}) (is-applied client/wrap-flatten-nested-params {:flatten-nested-keys [:foo]} {:flatten-nested-keys [:foo]}) (is-applied client/wrap-flatten-nested-params {:ignore-nested-query-string true} {:ignore-nested-query-string true :flatten-nested-keys []}) (is-applied client/wrap-flatten-nested-params {} {:flatten-nested-keys '(:query-params)}) (is-applied client/wrap-flatten-nested-params {:flatten-nested-form-params true} {:flatten-nested-form-params true :flatten-nested-keys '(:query-params :form-params)}) (is-applied client/wrap-flatten-nested-params {:flatten-nested-form-params true :ignore-nested-query-string true} {:ignore-nested-query-string true :flatten-nested-form-params true :flatten-nested-keys '(:form-params)}) (try ((client/wrap-flatten-nested-params identity) {:flatten-nested-form-params true :ignore-nested-query-string true :flatten-nested-keys [:thing :bar]}) (is false "should have thrown exception") (catch IllegalArgumentException e (is (= (.getMessage e) (str "only :flatten-nested-keys or :ignore-nested-query-string/" ":flatten-nested-keys may be specified, not both"))))) (try ((client/wrap-flatten-nested-params identity) {:ignore-nested-query-string true :flatten-nested-keys [:thing :bar]}) (is false "should have thrown exception") (catch IllegalArgumentException e (is (= (.getMessage e) (str "only :flatten-nested-keys or :ignore-nested-query-string/" ":flatten-nested-keys may be specified, not both")))))) clj-http-3.12.3/test/clj_http/test/conn_mgr_test.clj000066400000000000000000000153241407075231000224310ustar00rootroot00000000000000(ns clj-http.test.conn-mgr-test (:require [clj-http.conn-mgr :as conn-mgr] [clj-http.core :as core] [clj-http.test.core-test :refer [run-server]] [clojure.test :refer :all] [ring.adapter.jetty :as ring]) (:import java.security.KeyStore [javax.net.ssl KeyManagerFactory TrustManagerFactory] org.apache.http.impl.conn.BasicHttpClientConnectionManager)) (def client-ks "test-resources/client-keystore") (def client-ks-pass "keykey") (def secure-request {:request-method :get :uri "/" :server-port 18084 :scheme :https :keystore client-ks :keystore-pass client-ks-pass :trust-store client-ks :trust-store-pass client-ks-pass :server-name "localhost" :insecure? true}) (defn secure-handler [req] (if (nil? (:ssl-client-cert req)) {:status 403} {:status 200})) (deftest load-keystore (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey")] (is (instance? KeyStore ks)) (is (> (.size ks) 0)))) (deftest use-existing-keystore (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey") ks (conn-mgr/get-keystore ks)] (is (instance? KeyStore ks)) (is (> (.size ks) 0)))) (deftest load-keystore-with-nil-pass (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil nil)] (is (instance? KeyStore ks)))) (def array-of-trust-manager (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey") tmf (doto (TrustManagerFactory/getInstance (TrustManagerFactory/getDefaultAlgorithm)) (.init ks))] (.getTrustManagers tmf))) (def array-of-key-manager (let [ks (conn-mgr/get-keystore "test-resources/keystore" nil "keykey") tmf (doto (KeyManagerFactory/getInstance (KeyManagerFactory/getDefaultAlgorithm)) (.init ks (.toCharArray "keykey")))] (.getKeyManagers tmf))) (deftest ^:integration ssl-client-cert-get (let [server (ring/run-jetty secure-handler {:port 18083 :ssl-port 18084 :ssl? true :join? false :keystore "test-resources/keystore" :key-password "keykey" :client-auth :want})] (try (let [resp (core/request {:request-method :get :uri "/get" :server-port 18084 :scheme :https :insecure? true :server-name "localhost"})] (is (= 403 (:status resp)))) (let [resp (core/request secure-request)] (is (= 200 (:status resp)))) (finally (.stop server))))) (deftest ^:integration ssl-client-cert-get-async (let [server (ring/run-jetty secure-handler {:port 18083 :ssl-port 18084 :ssl? true :join? false :keystore "test-resources/keystore" :key-password "keykey" :client-auth :want})] (try (let [resp (promise) exception (promise) _ (core/request {:request-method :get :uri "/get" :server-port 18084 :scheme :https :insecure? true :server-name "localhost" :async? true} resp exception)] (is (= 403 (:status (deref resp 1000 {:status :timeout}))))) (let [resp (promise) exception (promise) _ (core/request (assoc secure-request :async? true) resp exception)] (is (= 200 (:status (deref resp 1000 {:status :timeout}))))) (testing "with reusable connection pool" (let [pool (conn-mgr/make-reusable-async-conn-manager {:timeout 10000 :keystore client-ks :keystore-pass client-ks-pass :trust-store client-ks :trust-store-pass client-ks-pass :insecure? true})] (try (let [resp (promise) exception (promise) _ (core/request {:request-method :get :uri "/get" :server-port 18084 :scheme :https :server-name "localhost" :connection-manager pool :async? true} resp exception)] (is (= 200 (:status (deref resp 1000 {:status :timeout})))) (is (:body @resp)) (is (not (realized? exception)))) (finally (conn-mgr/shutdown-manager pool))))) (finally (.stop server))))) (deftest ^:integration t-closed-conn-mgr-for-as-stream (run-server) (let [shutdown? (atom false) cm (proxy [BasicHttpClientConnectionManager] [] (shutdown [] (reset! shutdown? true)))] (try (core/request {:request-method :get :uri "/timeout" :server-port 18080 :scheme :http :server-name "localhost" ;; timeouts forces an exception being thrown :socket-timeout 1 :connection-timeout 1 :connection-manager cm :as :stream}) (is false "request should have thrown an exception") (catch Exception e)) (is @shutdown? "Connection manager has been shutdown"))) (deftest ^:integration t-closed-conn-mgr-for-empty-body (run-server) (let [shutdown? (atom false) cm (proxy [BasicHttpClientConnectionManager] [] (shutdown [] (reset! shutdown? true))) response (core/request {:request-method :get :uri "/unmodified-resource" :server-port 18080 :scheme :http :server-name "localhost" :connection-manager cm})] (is (nil? (:body response)) "response shouldn't have body") (is (= 304 (:status response))) (is @shutdown? "connection manager should be shutdown"))) (deftest t-reusable-conn-mgrs (let [regular (conn-mgr/make-regular-conn-manager {}) regular-reusable (conn-mgr/make-reusable-conn-manager {}) async (conn-mgr/make-regular-async-conn-manager {}) async-reusable (conn-mgr/make-reusable-async-conn-manager {}) async-reuseable (conn-mgr/make-reuseable-async-conn-manager {})] (is (false? (conn-mgr/reusable? regular))) (is (true? (conn-mgr/reusable? regular-reusable))) (is (false? (conn-mgr/reusable? async))) (is (true? (conn-mgr/reusable? async-reusable))) (is (true? (conn-mgr/reusable? async-reuseable))))) clj-http-3.12.3/test/clj_http/test/cookies_test.clj000066400000000000000000000203241407075231000222570ustar00rootroot00000000000000(ns clj-http.test.cookies-test (:require [clj-http.cookies :refer :all] [clojure.test :refer :all]) (:import [org.apache.http.impl.cookie BasicClientCookie BasicClientCookie2])) (defn refer-private [ns] (doseq [[symbol var] (ns-interns ns)] (when (:private (meta var)) (intern *ns* symbol var)))) (refer-private 'clj-http.cookies) (def session (str "ltQGXSNp7cgNeFG6rPE06qzriaI+R8W7zJKFu4UOlX4=-" "-lWgojFmZlDqSBnYJlUmwhqXL4OgBTkra5WXzi74v+nE=")) (deftest test-compact-map (are [map expected] (is (= expected (compact-map map))) {:a nil :b 2 :c 3 :d nil} {:b 2 :c 3} {:comment nil :domain "example.com" :path "/" :ports [80 8080] :value 1} {:domain "example.com" :path "/" :ports [80 8080] :value 1})) (deftest test-decode-cookie (are [set-cookie-str expected] (is (= expected (decode-cookie set-cookie-str))) nil nil "" nil "example-cookie=example-value;Path=/" ["example-cookie" {:discard true :path "/" :secure false :value "example-value" :version 0}] "example-cookie=example-value;Domain=.example.com;Path=/" ["example-cookie" {:discard true :domain "example.com" :secure false :path "/" :value "example-value" :version 0}])) (deftest test-decode-cookies-with-seq (let [cookies (decode-cookies [(str "ring-session=" session)])] (is (map? cookies)) (is (= 1 (count cookies))) (let [cookie (get cookies "ring-session")] (is (= true (:discard cookie))) (is (nil? (:domain cookie))) (is (= "/" (:path cookie))) (is (= session (:value cookie))) (is (= 0 (:version cookie)))))) (deftest test-decode-cookies-with-string (let [cookies (decode-cookies (str "ring-session=" session ";Path=/"))] (is (map? cookies)) (is (= 1 (count cookies))) (let [cookie (get cookies "ring-session")] (is (= true (:discard cookie))) (is (nil? (:domain cookie))) (is (= "/" (:path cookie))) (is (= session (:value cookie))) (is (= 0 (:version cookie)))))) (deftest test-decode-cookie-header (are [response expected] (is (= expected (decode-cookie-header response))) {:headers {"set-cookie" "a=1"}} {:cookies {"a" {:discard true :path "/" :secure false :value "1" :version 0}} :headers {}} {:headers {"set-cookie" (str "ring-session=" session ";Path=/")}} {:cookies {"ring-session" {:discard true :path "/" :secure false :value session :version 0}} :headers {}})) (deftest test-encode-cookie (are [cookie expected] (is (= expected (encode-cookie cookie))) [:a {:value "b"}] "a=b" ["a" {:value "b"}] "a=b" ["example-cookie" {:domain ".example.com" :path "/" :value "example-value"}] "example-cookie=example-value" ["ring-session" {:value session}] (str "ring-session=" session))) (deftest test-encode-cookies (are [cookie expected] (is (= expected (encode-cookies cookie))) (sorted-map :a {:value "b"} :c {:value "d"} :e {:value "f"}) "a=b;c=d;e=f" (sorted-map "a" {:value "b"} "c" {:value "d"} "e" {:value "f"}) "a=b;c=d;e=f" {"example-cookie" {:domain ".example.com" :path "/" :value "example-value"}} "example-cookie=example-value" {"example-cookie" {:domain ".example.com" :path "/" :value "example-value" :discard true :version 0}} "example-cookie=example-value" {"ring-session" {:value session}} (str "ring-session=" session))) (deftest test-encode-cookie-header (are [request expected] (is (= expected (encode-cookie-header request))) {:cookies {"a" {:value "1"}}} {:headers {"Cookie" "a=1"}} {:cookies {"example-cookie" {:domain ".example.com" :path "/" :value "example-value"}}} {:headers {"Cookie" "example-cookie=example-value"}})) (deftest test-to-basic-client-cookie-with-simple-cookie (let [cookie (to-basic-client-cookie ["ring-session" {:value session :path "/" :domain "example.com"}])] (is (= "ring-session" (.getName cookie))) (is (= session (.getValue cookie))) (is (= "/" (.getPath cookie))) (is (= "example.com" (.getDomain cookie))) (is (nil? (.getComment cookie))) (is (nil? (.getCommentURL cookie))) (is (not (.isPersistent cookie))) (is (nil? (.getExpiryDate cookie))) (is (nil? (seq (.getPorts cookie)))) (is (not (.isSecure cookie))) (is (= 0 (.getVersion cookie))))) (deftest test-to-basic-client-cookie-with-full-cookie (let [cookie (to-basic-client-cookie ["ring-session" {:value session :path "/" :domain "example.com" :comment "Example Comment" :comment-url "http://example.com/cookies" :discard true :expires (java.util.Date. (long 0)) :ports [80 8080] :secure true :version 0}])] (is (= "ring-session" (.getName cookie))) (is (= session (.getValue cookie))) (is (= "/" (.getPath cookie))) (is (= "example.com" (.getDomain cookie))) (is (= "Example Comment" (.getComment cookie))) (is (= "http://example.com/cookies" (.getCommentURL cookie))) (is (not (.isPersistent cookie))) (is (= (java.util.Date. (long 0)) (.getExpiryDate cookie))) (is (= [80 8080] (seq (.getPorts cookie)))) (is (.isSecure cookie)) (is (= 0 (.getVersion cookie))))) (deftest test-to-basic-client-cookie-with-symbol-as-name (let [cookie (to-basic-client-cookie [:ring-session {:value session :path "/" :domain "example.com"}])] (is (= "ring-session" (.getName cookie))))) (deftest test-to-cookie-with-simple-cookie (let [[name content] (to-cookie (doto (BasicClientCookie. "example-cookie" "example-value") (.setDomain "example.com") (.setPath "/")))] (is (= "example-cookie" name)) (is (nil? (:comment content))) (is (nil? (:comment-url content))) (is (:discard content)) (is (= "example.com" (:domain content))) (is (nil? (:expires content))) (is (nil? (:ports content))) (is (not (:secure content))) (is (= 0 (:version content))) (is (= "example-value" (:value content))))) (deftest test-to-cookie-with-full-cookie (let [[name content] (to-cookie (doto (BasicClientCookie2. "example-cookie" "example-value") (.setComment "Example Comment") (.setCommentURL "http://example.com/cookies") (.setDiscard true) (.setDomain "example.com") (.setExpiryDate (java.util.Date. (long 0))) (.setPath "/") (.setPorts (int-array [80 8080])) (.setSecure true) (.setVersion 1)))] (is (= "example-cookie" name)) (is (= "Example Comment" (:comment content))) (is (= "http://example.com/cookies" (:comment-url content))) (is (= true (:discard content))) (is (= "example.com" (:domain content))) (is (= (java.util.Date. (long 0)) (:expires content))) (is (= [80 8080] (:ports content))) (is (= true (:secure content))) (is (= 1 (:version content))) (is (= "example-value" (:value content))))) (deftest test-wrap-cookies (is (= {:cookies {"example-cookie" {:discard true :domain "example.com" :path "/" :value "example-value" :version 0 :secure false}} :headers {}} ((wrap-cookies (fn [request] (is (= (get (:headers request) "Cookie") "a=1;b=2")) {:headers {"set-cookie" "example-cookie=example-value;Domain=.example.com;Path=/"}})) {:cookies (sorted-map :a {:value "1"} :b {:value "2"})}))) (is (= {:headers {"set-cookie" "example-cookie=example-value;Domain=.example.com;Path=/"}} ((wrap-cookies (fn [request] (is (= (get (:headers request) "Cookie") "a=1;b=2")) {:headers {"set-cookie" "example-cookie=example-value;Domain=.example.com;Path=/"}})) {:cookies (sorted-map :a {:value "1"} :b {:value "2"}) :decode-cookies false})))) clj-http-3.12.3/test/clj_http/test/core_test.clj000066400000000000000000001230701407075231000215550ustar00rootroot00000000000000(ns clj-http.test.core-test (:require [cheshire.core :as json] [clj-http.client :as client] [clj-http.conn-mgr :as conn] [clj-http.core :as core] [clj-http.util :as util] [clojure.java.io :refer [file]] [clojure.test :refer :all] [ring.adapter.jetty :as ring]) (:import java.io.ByteArrayInputStream [java.net InetAddress SocketTimeoutException] [java.util.concurrent TimeoutException TimeUnit] [org.apache.http HttpConnection HttpInetConnection HttpRequest HttpResponse ProtocolException] org.apache.http.client.config.RequestConfig org.apache.http.client.params.ClientPNames org.apache.http.client.protocol.HttpClientContext org.apache.http.impl.conn.InMemoryDnsResolver org.apache.http.impl.cookie.RFC6265CookieSpecProvider [org.apache.http.message BasicHeader BasicHeaderIterator] [org.apache.http.params CoreConnectionPNames CoreProtocolPNames] [org.apache.http.protocol ExecutionContext HttpContext] org.apache.logging.log4j.LogManager sun.security.provider.certpath.SunCertPathBuilderException)) (defonce logger (LogManager/getLogger "clj-http.test.core-test")) (defn handler [req] (condp = [(:request-method req) (:uri req)] [:get "/get"] {:status 200 :body "get"} [:get "/dont-cache"] {:status 200 :body "nocache" :headers {"cache-control" "private"}} [:get "/empty"] {:status 200 :body nil} [:get "/empty-gzip"] {:status 200 :body nil :headers {"content-encoding" "gzip"}} [:get "/clojure"] {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}" :headers {"content-type" "application/clojure"}} [:get "/edn"] {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}" :headers {"content-type" "application/edn"}} [:get "/clojure-bad"] {:status 200 :body "{:foo \"bar\" :baz #=(+ 1 1)}" :headers {"content-type" "application/clojure"}} [:get "/json"] {:status 200 :body "{\"foo\":\"bar\"}" :headers {"content-type" "application/json"}} [:get "/json-array"] {:status 200 :body "[\"foo\", \"bar\"]" :headers {"content-type" "application/json"}} [:get "/json-large-array"] {:status 200 :body (file "test-resources/big_array_json.json") :headers {"content-type" "application/json"}} [:get "/json-bad"] {:status 400 :body "{\"foo\":\"bar\"}"} [:get "/redirect"] {:status 302 :headers {"location" "http://localhost:18080/redirect"}} [:get "/bad-redirect"] {:status 301 :headers {"location" "https:///"}} [:get "/redirect-to-get"] {:status 302 :headers {"location" "http://localhost:18080/get"}} [:get "/unmodified-resource"] {:status 304} [:get "/transit-json"] {:status 200 :body (str "[\"^ \",\"~:eggplant\",[\"^ \",\"~:quux\"," "[\"~#set\",[1,3,2]]],\"~:baz\",\"~f7\"," "\"~:foo\",\"bar\"]") :headers {"content-type" "application/transit+json"}} [:get "/transit-json-bad"] {:status 400 :body "[\"^ \", \"~:foo\",\"bar\"]"} [:get "/transit-json-empty"] {:status 200 :headers {"content-type" "application/transit+json"}} [:get "/transit-msgpack"] {:status 200 :body (->> [-125 -86 126 58 101 103 103 112 108 97 110 116 -127 -90 126 58 113 117 117 120 -110 -91 126 35 115 101 116 -109 1 3 2 -91 126 58 98 97 122 -93 126 102 55 -91 126 58 102 111 111 -93 98 97 114] (map byte) (byte-array) (ByteArrayInputStream.)) :headers {"content-type" "application/transit+msgpack"}} [:head "/head"] {:status 200} [:get "/content-type"] {:status 200 :body (:content-type req)} [:get "/header"] {:status 200 :body (get-in req [:headers "x-my-header"])} [:post "/post"] {:status 200 :body (:body req)} [:get "/error"] {:status 500 :body "o noes"} [:get "/timeout"] (do (Thread/sleep 10) {:status 200 :body "timeout"}) [:delete "/delete-with-body"] {:status 200 :body "delete-with-body"} [:post "/multipart"] {:status 200 :body (:body req)} [:get "/get-with-body"] {:status 200 :body (:body req)} [:options "/options"] {:status 200 :body "options"} [:copy "/copy"] {:status 200 :body "copy"} [:move "/move"] {:status 200 :body "move"} [:patch "/patch"] {:status 200 :body "patch"} [:get "/headers"] {:status 200 :body (json/encode (:headers req))} [:propfind "/propfind"] {:status 200 :body "propfind"} [:propfind "/propfind-with-body"] {:status 200 :body (:body req)} [:get "/query-string"] {:status 200 :body (:query-string req)} [:get "/cookie"] {:status 200 :body "yay" :headers {"Set-Cookie" "foo=bar"}} [:get "/bad-cookie"] {:status 200 :body "yay" :headers {"Set-Cookie" (str "DD-PSHARD=3; expires=\"Thu, 12-Apr-2018 06:40:25 GMT\"; " "Max-Age=604800; Path=/; secure; HttpOnly")}})) (defn add-headers-if-requested [client] (fn [req] (let [resp (client req) add-all (-> req :headers (get "add-headers")) headers (:headers resp)] (if add-all (assoc resp :headers (assoc headers "got" (pr-str (:headers req)))) resp)))) (defn run-server [] (defonce server (ring/run-jetty (add-headers-if-requested #'handler) {:port 18080 :join? false}))) (defn localhost [path] (str "http://localhost:18080" path)) (def base-req {:scheme :http :server-name "localhost" :server-port 18080}) (defn request [req] (core/request (merge base-req req))) (defn slurp-body [req] (slurp (:body req))) (deftest ^:integration makes-get-request (run-server) (let [resp (request {:request-method :get :uri "/get"})] (is (= 200 (:status resp))) (is (= "get" (slurp-body resp))))) (deftest ^:integration dns-resolver (run-server) (let [custom-dns-resolver (doto (InMemoryDnsResolver.) (.add "foo.bar.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))]))) resp (request {:request-method :get :uri "/get" :server-name "foo.bar.com" :dns-resolver custom-dns-resolver})] (is (= 200 (:status resp))) (is (= "get" (slurp-body resp))))) (deftest ^:integration dns-resolver-unknown-host (run-server) (let [custom-dns-resolver (doto (InMemoryDnsResolver.) (.add "foo.bar.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))])))] (is (thrown? java.net.UnknownHostException (request {:request-method :get :uri "/get" :server-name "www.google.com" :dns-resolver custom-dns-resolver}))))) (deftest ^:integration dns-resolver-reusable-connection-manager (run-server) (let [custom-dns-resolver (doto (InMemoryDnsResolver.) (.add "totallynonexistant.google.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))]))) cm (conn/make-reuseable-async-conn-manager {:dns-resolver custom-dns-resolver}) hc (core/build-async-http-client {} cm)] (client/get "http://totallynonexistant.google.com:18080/json" {:connection-manager cm :http-client hc :as :json :async true} (fn [resp] (is (= 200 (:status resp))) (is (= {:foo "bar"} (:body resp)))) (fn [e] (is false (str "failed with " e))))) (let [custom-dns-resolver (doto (InMemoryDnsResolver.) (.add "nonexistant.google.com" (into-array[(InetAddress/getByAddress (byte-array [127 0 0 1]))]))) cm (conn/make-reusable-conn-manager {:dns-resolver custom-dns-resolver}) hc (:http-client (client/get "http://nonexistant.google.com:18080/get" {:connection-manager cm})) resp (client/get "http://nonexistant.google.com:18080/json" {:connection-manager cm :http-client hc :as :json})] (is (= 200 (:status resp))) (is (= {:foo "bar"} (:body resp))))) (deftest ^:integration save-request-option (run-server) (let [resp (request {:request-method :post :uri "/post" :body (util/utf8-bytes "contents") :save-request? true})] (is (= "/post" (-> resp :request :uri))))) (deftest ^:integration makes-head-request (run-server) (let [resp (request {:request-method :head :uri "/head"})] (is (= 200 (:status resp))) (is (nil? (:body resp))))) (deftest ^:integration sets-content-type-with-charset (run-server) (let [resp (client/request {:scheme :http :server-name "localhost" :server-port 18080 :request-method :get :uri "/content-type" :content-type "text/plain" :character-encoding "UTF-8"})] (is (= "text/plain; charset=UTF-8" (:body resp))))) (deftest ^:integration sets-content-type-without-charset (run-server) (let [resp (client/request {:scheme :http :server-name "localhost" :server-port 18080 :request-method :get :uri "/content-type" :content-type "text/plain"})] (is (= "text/plain" (:body resp))))) (deftest ^:integration sets-arbitrary-headers (run-server) (let [resp (request {:request-method :get :uri "/header" :headers {"x-my-header" "header-val"}})] (is (= "header-val" (slurp-body resp))))) (deftest ^:integration sends-and-returns-byte-array-body (run-server) (let [resp (request {:request-method :post :uri "/post" :body (util/utf8-bytes "contents")})] (is (= 200 (:status resp))) (is (= "contents" (slurp-body resp))))) (deftest ^:integration returns-arbitrary-headers (run-server) (let [resp (request {:request-method :get :uri "/get"})] (is (string? (get-in resp [:headers "date"]))) (is (nil? (get-in resp [:headers "Date"]))))) (deftest ^:integration returns-status-on-exceptional-responses (run-server) (let [resp (request {:request-method :get :uri "/error"})] (is (= 500 (:status resp))))) (deftest ^:integration sets-socket-timeout (run-server) (try (is (thrown? SocketTimeoutException (client/request {:scheme :http :server-name "localhost" :server-port 18080 :request-method :get :uri "/timeout" :socket-timeout 1}))))) (deftest ^:integration delete-with-body (run-server) (let [resp (request {:request-method :delete :uri "/delete-with-body" :body (.getBytes "foo bar")})] (is (= 200 (:status resp))))) (deftest ^:integration self-signed-ssl-get (let [server (ring/run-jetty handler {:port 8081 :ssl-port 18082 :ssl? true :join? false :keystore "test-resources/keystore" :key-password "keykey"})] (try (is (thrown? SunCertPathBuilderException (client/request {:scheme :https :server-name "localhost" :server-port 18082 :request-method :get :uri "/get"}))) (let [resp (request {:request-method :get :uri "/get" :server-port 18082 :scheme :https :insecure? true})] (is (= 200 (:status resp))) (is (= "get" (String. (util/force-byte-array (:body resp)))))) (finally (.stop server))))) (deftest ^:integration multipart-form-uploads (run-server) (let [bytes (util/utf8-bytes "byte-test") stream (ByteArrayInputStream. bytes) resp (request {:request-method :post :uri "/multipart" :multipart [{:name "a" :content "testFINDMEtest" :encoding "UTF-8" :mime-type "application/text"} {:name "b" :content bytes :mime-type "application/json"} {:name "d" :content (file "test-resources/keystore") :encoding "UTF-8" :mime-type "application/binary"} {:name "c" :content stream :mime-type "application/json"} {:name "e" :part-name "eggplant" :content "content" :mime-type "application/text"}]}) resp-body (apply str (map #(try (char %) (catch Exception _ "")) (util/force-byte-array (:body resp))))] (is (= 200 (:status resp))) (is (re-find #"testFINDMEtest" resp-body)) (is (re-find #"application/json" resp-body)) (is (re-find #"application/text" resp-body)) (is (re-find #"UTF-8" resp-body)) (is (re-find #"byte-test" resp-body)) (is (re-find #"name=\"c\"" resp-body)) (is (re-find #"name=\"d\"" resp-body)) (is (re-find #"name=\"eggplant\"" resp-body)) (is (re-find #"content" resp-body)))) (deftest ^:integration multipart-inputstream-length (run-server) (let [bytes (util/utf8-bytes "byte-test") stream (ByteArrayInputStream. bytes) resp (request {:request-method :post :uri "/multipart" :multipart [{:name "c" :content stream :length 9 :mime-type "application/json"}]}) resp-body (apply str (map #(try (char %) (catch Exception _ "")) (util/force-byte-array (:body resp))))] (is (= 200 (:status resp))) (is (re-find #"byte-test" resp-body)))) (deftest parse-headers (are [headers expected] (let [iterator (BasicHeaderIterator. (into-array BasicHeader (map (fn [[name value]] (BasicHeader. name value)) headers)) nil)] (is (= (core/parse-headers iterator) expected))) [] {} [["Set-Cookie" "one"]] {"set-cookie" "one"} [["Set-Cookie" "one"] ["set-COOKIE" "two"]] {"set-cookie" ["one" "two"]} [["Set-Cookie" "one"] ["serVer" "some-server"] ["set-cookie" "two"]] {"set-cookie" ["one" "two"] "server" "some-server"})) (deftest ^:integration t-streaming-response (run-server) (let [stream (:body (request {:request-method :get :uri "/get" :as :stream})) body (slurp stream)] (is (= "get" body)))) (deftest ^:integration throw-on-too-many-redirects (run-server) (let [resp (client/get (localhost "/redirect") {:max-redirects 2 :throw-exceptions false :redirect-strategy :none :allow-circular-redirects true})] (is (= 302 (:status resp)))) (let [resp (client/get (localhost "/redirect") {:max-redirects 3 :redirect-strategy :graceful :allow-circular-redirects true})] (is (= 302 (:status resp))) (is (= 3 (count (:trace-redirects resp)))) (is (= ["http://localhost:18080/redirect" "http://localhost:18080/redirect" "http://localhost:18080/redirect"] (:trace-redirects resp)))) (is (thrown-with-msg? Exception #"Maximum redirects \(2\) exceeded" (client/get (localhost "/redirect") {:max-redirects 2 :throw-exceptions true :allow-circular-redirects true}))) (is (thrown-with-msg? Exception #"Maximum redirects \(50\) exceeded" (client/get (localhost "/redirect") {:throw-exceptions true :allow-circular-redirects true})))) (deftest ^:integration get-with-body (run-server) (let [resp (request {:request-method :get :uri "/get-with-body" :body (.getBytes "foo bar")})] (is (= 200 (:status resp))) (is (= "foo bar" (String. (util/force-byte-array (:body resp))))))) (deftest ^:integration head-with-body (run-server) (let [resp (request {:request-method :head :uri "/head" :body "foo"})] (is (= 200 (:status resp))))) (deftest ^:integration t-clojure-output-coercion (run-server) (let [resp (client/get (localhost "/clojure") {:as :clojure})] (is (= 200 (:status resp))) (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body resp)))) (let [clj-resp (client/get (localhost "/clojure") {:as :auto}) edn-resp (client/get (localhost "/edn") {:as :auto})] (is (= 200 (:status clj-resp) (:status edn-resp))) (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body clj-resp) (:body edn-resp))))) (deftest ^:integration t-transit-output-coercion (run-server) (let [transit-json-resp (client/get (localhost "/transit-json") {:as :auto}) transit-msgpack-resp (client/get (localhost "/transit-msgpack") {:as :auto}) bad-status-resp-default (client/get (localhost "/transit-json-bad") {:throw-exceptions false :as :transit+json}) bad-status-resp-always (client/get (localhost "/transit-json-bad") {:throw-exceptions false :as :transit+json :coerce :always}) bad-status-resp-exceptional (client/get (localhost "/transit-json-bad") {:throw-exceptions false :as :transit+json :coerce :exceptional}) empty-resp (client/get (localhost "/transit-json-empty") {:throw-exceptions false :as :transit+json})] (is (= 200 (:status transit-json-resp) (:status transit-msgpack-resp) (:status empty-resp))) (is (= 400 (:status bad-status-resp-default) (:status bad-status-resp-always) (:status bad-status-resp-exceptional))) (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body transit-json-resp) (:body transit-msgpack-resp))) (is (nil? (:body empty-resp))) (is (= "[\"^ \", \"~:foo\",\"bar\"]" (:body bad-status-resp-default))) (is (= {:foo "bar"} (:body bad-status-resp-always))) (is (= {:foo "bar"} (:body bad-status-resp-exceptional))))) (deftest ^:integration t-json-output-coercion (run-server) (let [resp (client/get (localhost "/json") {:as :json}) resp-array (client/get (localhost "/json-array") {:as :json}) resp-array-strict (client/get (localhost "/json-array") {:as :json-strict}) resp-large-array (client/get (localhost "/json-large-array") {:as :json}) resp-large-array-strict (client/get (localhost "/json-large-array") {:as :json-strict}) resp-str (client/get (localhost "/json") {:as :json :coerce :exceptional}) resp-str-keys (client/get (localhost "/json") {:as :json-string-keys}) resp-strict-str-keys (client/get (localhost "/json") {:as :json-strict-string-keys}) resp-auto (client/get (localhost "/json") {:as :auto}) bad-resp (client/get (localhost "/json-bad") {:throw-exceptions false :as :json}) bad-resp-json (client/get (localhost "/json-bad") {:throw-exceptions false :as :json :coerce :always}) bad-resp-json2 (client/get (localhost "/json-bad") {:throw-exceptions false :as :json :coerce :unexceptional})] (is (= 200 (:status resp) (:status resp-array) (:status resp-array-strict) (:status resp-large-array) (:status resp-large-array-strict) (:status resp-str) (:status resp-str-keys) (:status resp-strict-str-keys) (:status resp-auto))) (is (= {:foo "bar"} (:body resp) (:body resp-auto))) (is (= ["foo", "bar"] (:body resp-array))) (is (= {"foo" "bar"} (:body resp-strict-str-keys) (:body resp-str-keys))) ;; '("foo" "bar") and ["foo" "bar"] compare as equal with =. (is (vector? (:body resp-array))) (is (vector? (:body resp-array-strict))) (is (= "{\"foo\":\"bar\"}" (:body resp-str))) (is (= 400 (:status bad-resp) (:status bad-resp-json) (:status bad-resp-json2))) (is (= "{\"foo\":\"bar\"}" (:body bad-resp)) "don't coerce on bad response status by default") (is (= {:foo "bar"} (:body bad-resp-json))) (is (= "{\"foo\":\"bar\"}" (:body bad-resp-json2))) (testing "lazily parsed stream completes parsing." (is (= 100 (count (:body resp-large-array))))) (is (= 100 (count (:body resp-large-array-strict)))))) (deftest ^:integration t-ipv6 (run-server) (let [resp (client/get "http://[::1]:18080/get")] (is (= 200 (:status resp))) (is (= "get" (:body resp))))) (deftest t-custom-retry-handler (let [called? (atom false)] (is (thrown? Exception (client/post "http://localhost" {:multipart [{:name "title" :content "Foo"} {:name "Content/type" :content "text/plain"} {:name "file" :content (file "/tmp/missingfile")}] :retry-handler (fn [ex try-count http-context] (reset! called? true) false)}))) (is @called?))) ;; super-basic test for methods that aren't used that often (deftest ^:integration t-copy-options-move (run-server) (let [resp1 (client/options (localhost "/options")) resp2 (client/move (localhost "/move")) resp3 (client/copy (localhost "/copy")) resp4 (client/patch (localhost "/patch"))] (is (= #{200} (set (map :status [resp1 resp2 resp3 resp4])))) (is (= "options" (:body resp1))) (is (= "move" (:body resp2))) (is (= "copy" (:body resp3))) (is (= "patch" (:body resp4))))) (deftest ^:integration t-json-encoded-form-params (run-server) (let [params {:param1 "value1" :param2 {:foo "bar"}} resp (client/post (localhost "/post") {:content-type :json :form-params params})] (is (= 200 (:status resp))) (is (= (json/encode params) (:body resp))))) (deftest ^:integration t-request-interceptor (run-server) (let [req-ctx (atom []) {:keys [status trace-redirects] :as resp} (client/get (localhost "/get") {:request-interceptor (fn [^HttpRequest req ^HttpContext ctx] (reset! req-ctx {:method (.getMethod req) :uri (.getURI req)}))})] (is (= 200 status)) (is (= "GET" (:method @req-ctx))) (is (= "/get" (.getPath (:uri @req-ctx)))))) (deftest ^:integration t-response-interceptor (run-server) (let [saved-ctx (atom []) {:keys [status trace-redirects] :as resp} (client/get (localhost "/redirect-to-get") {:response-interceptor (fn [^HttpResponse resp ^HttpContext ctx] (let [^HttpInetConnection conn (.getAttribute ctx ExecutionContext/HTTP_CONNECTION)] (swap! saved-ctx conj {:remote-port (.getRemotePort conn) :http-conn conn})))})] (is (= 200 status)) (is (= 2 (count @saved-ctx))) #_(is (= (count trace-redirects) (count @saved-ctx))) (is (every? #(= 18080 (:remote-port %)) @saved-ctx)) (is (every? #(instance? HttpConnection (:http-conn %)) @saved-ctx)))) (deftest ^:integration t-send-input-stream-body (run-server) (let [b1 (:body (client/post "http://localhost:18080/post" {:body (ByteArrayInputStream. (.getBytes "foo")) :length 3})) b2 (:body (client/post "http://localhost:18080/post" {:body (ByteArrayInputStream. (.getBytes "foo"))})) b3 (:body (client/post "http://localhost:18080/post" {:body (ByteArrayInputStream. (.getBytes "apple")) :length 2}))] (is (= b1 "foo")) (is (= b2 "foo")) (is (= b3 "ap")))) ;; (deftest t-add-client-params ;; (testing "Using add-client-params!" ;; (let [ps {"http.conn-manager.timeout" 100 ;; "http.socket.timeout" 250 ;; "http.protocol.allow-circular-redirects" false ;; "http.protocol.version" HttpVersion/HTTP_1_0 ;; "http.useragent" "clj-http"} ;; setps (.getParams (doto (DefaultHttpClient.) ;; (core/add-client-params! ps)))] ;; (doseq [[k v] ps] ;; (is (= v (.getParameter setps k))))))) ;; Regression, get notified if something changes (deftest ^:integration t-known-client-params-are-unchanged (let [params ["http.socket.timeout" CoreConnectionPNames/SO_TIMEOUT "http.connection.timeout" CoreConnectionPNames/CONNECTION_TIMEOUT "http.protocol.version" CoreProtocolPNames/PROTOCOL_VERSION "http.useragent" CoreProtocolPNames/USER_AGENT "http.conn-manager.timeout" ClientPNames/CONN_MANAGER_TIMEOUT "http.protocol.allow-circular-redirects" ClientPNames/ALLOW_CIRCULAR_REDIRECTS]] (doseq [[plaintext constant] (partition 2 params)] (is (= plaintext constant))))) ;; If you don't explicitly set a :cookie-policy, use ;; CookiePolicy/BROWSER_COMPATIBILITY ;; (deftest t-add-client-params-default-cookie-policy ;; (testing "Using add-client-params! to get a default cookie policy" ;; (let [setps (.getParams (doto (DefaultHttpClient.) ;; (core/add-client-params! {})))] ;; (is (= CookiePolicy/BROWSER_COMPATIBILITY ;; (.getParameter setps ClientPNames/COOKIE_POLICY)))))) ;; If you set a :cookie-policy, the name of the policy is registered ;; as (str (type cookie-policy)) ;; (deftest t-add-client-params-cookie-policy ;; (testing "Using add-client-params! to get an explicitly set :cookie-policy" ;; (let [setps (.getParams (doto (DefaultHttpClient.) ;; (core/add-client-params! ;; {:cookie-policy (constantly nil)})))] ;; (is (.startsWith ^String ;; (.getParameter setps ClientPNames/COOKIE_POLICY) ;; "class "))))) ;; This relies on connections to writequit.org being slower than 10ms, if this ;; fails, you must have very nice internet. (deftest ^:integration sets-connection-timeout (run-server) (try (is (thrown? SocketTimeoutException (client/request {:scheme :http :server-name "writequit.org" :server-port 80 :request-method :get :uri "/" :connection-timeout 10}))))) (deftest ^:integration connection-pool-timeout (run-server) (client/with-connection-pool {:threads 1 :default-per-route 1} (let [async-request #(future (client/request {:scheme :http :server-name "localhost" :server-port 18080 :request-method :get :connection-timeout 1 :connection-request-timeout 1 :uri "/timeout"})) is-pool-timeout-error? (fn [req-fut] (instance? org.apache.http.conn.ConnectionPoolTimeoutException (try @req-fut (catch Exception e (.getCause e))))) req1 (async-request) req2 (async-request) timeout-error1 (is-pool-timeout-error? req1) timeout-error2 (is-pool-timeout-error? req2)] (is (or timeout-error1 timeout-error2))))) (deftest ^:integration t-header-collections (run-server) (let [headers (-> (client/get "http://localhost:18080/headers" {:headers {"foo" ["bar" "baz"] "eggplant" "quux"}}) :body json/decode)] (is (= {"eggplant" "quux" "foo" "bar,baz"} (select-keys headers ["foo" "eggplant"]))))) (deftest ^:integration t-clojure-no-read-eval (run-server) (is (thrown? Exception (client/get (localhost "/clojure-bad") {:as :clojure})) "Should throw an exception when reading clojure eval components")) (deftest ^:integration t-numeric-headers (run-server) (client/request {:method :get :url (localhost "/get") :headers {"foo" 2}})) (deftest ^:integration t-empty-response-coercion (run-server) (let [resp (client/get (localhost "/empty") {:as :clojure})] (is (= (:body resp) nil))) (let [resp (client/get (localhost "/empty") {:as :json})] (is (= (:body resp) nil))) (let [resp (client/get (localhost "/empty-gzip") {:as :clojure})] (is (= (:body resp) nil))) (let [resp (client/get (localhost "/empty-gzip") {:as :json})] (is (= (:body resp) nil)))) (deftest ^:integration t-trace-redirects (run-server) (let [resp-with-redirects (client/request {:method :get :url (localhost "/redirect-to-get")}) resp-with-graceful-redirects (client/request {:method :get :url (localhost "/redirect-to-get") :redirect-strategy :graceful}) resp-without-redirects (client/request {:method :get :url (localhost "/redirect-to-get") :redirect-strategy :none})] (is (= (:trace-redirects resp-with-redirects) ["http://localhost:18080/get"])) (is (= (:trace-redirects resp-with-graceful-redirects) ["http://localhost:18080/get"])) (is (= (:trace-redirects resp-without-redirects) [])))) (deftest t-request-config (let [params {:conn-timeout 100 ;; deprecated :connection-timeout 200 ;; takes precedence over `:conn-timeout` :conn-request-timeout 300 ;; deprecated :connection-request-timeout 400 ;; takes precedence over `:conn-request-timeout` :socket-timeout 500 :max-redirects 600 :cookie-spec "foo" :normalize-uri false} request-config (core/request-config params)] (is (= 200 (.getConnectTimeout request-config))) (is (= 400 (.getConnectionRequestTimeout request-config))) (is (= 500 (.getSocketTimeout request-config))) (is (= 600 (.getMaxRedirects request-config))) (is (= core/CUSTOM_COOKIE_POLICY (.getCookieSpec request-config))) (is (false? (.isNormalizeUri request-config))))) (deftest ^:integration t-override-request-config (run-server) (let [called-args (atom []) real-http-client core/build-http-client http-context (HttpClientContext/create) request-config (.build (RequestConfig/custom))] (with-redefs [core/build-http-client (fn [& args] (proxy [org.apache.http.impl.client.CloseableHttpClient] [] (execute [http-req context] (swap! called-args conj [http-req context]) (.execute (apply real-http-client args) http-req context))))] (client/request {:method :get :url "http://localhost:18080/get" :http-client-context http-context :http-request-config request-config}) (let [context-for-request (last (last @called-args))] (is (= http-context context-for-request)) (is (= request-config (.getRequestConfig context-for-request))))))) (deftest ^:integration test-custom-http-builder-fns (run-server) (let [resp (client/get (localhost "/get") {:headers {"add-headers" "true"} :http-builder-fns [(fn [builder req] (.setDefaultHeaders builder (:hdrs req)))] :hdrs [(BasicHeader. "foo" "bar")]})] (is (= 200 (:status resp))) (is (.contains (get-in resp [:headers "got"]) "\"foo\" \"bar\"") "Headers should have included the new default headers")) (let [resp (promise) error (promise) f (client/get (localhost "/get") {:async true :headers {"add-headers" "true"} :async-http-builder-fns [(fn [builder req] (.setDefaultHeaders builder (:hdrs req)))] :hdrs [(BasicHeader. "foo" "bar")]} resp error)] (.get f) (is (= 200 (:status @resp))) (is (.contains (get-in @resp [:headers "got"]) "\"foo\" \"bar\"") "Headers should have included the new default headers") (is (not (realized? error))))) (deftest ^:integration test-custom-http-client-builder (run-server) (let [methods (atom nil) resp (client/get (localhost "/get") {:http-client-builder (-> (org.apache.http.impl.client.HttpClientBuilder/create) (.setRequestExecutor (proxy [org.apache.http.protocol.HttpRequestExecutor] [] (execute [request connection context] (->> request .getRequestLine .getMethod (swap! methods conj)) (proxy-super execute request connection context)))))})] (is (= ["GET"] @methods)))) (deftest ^:integration test-bad-redirects (run-server) (try (client/get (localhost "/bad-redirect")) (is false "should have thrown an exception") (catch ProtocolException e (is (.contains (.getMessage e) "Redirect URI does not specify a valid host name: https:///")))) ;; async version (let [e (atom nil) latch (promise)] (try (.get (client/get (localhost "/bad-redirect") {:async true} (fn [resp] (is false (str "should not have been called but got" resp))) (fn [err] (reset! e err) (deliver latch true) nil))) (catch Exception error (is (.contains (.getMessage error) "Redirect URI does not specify a valid host name: https:///")))) @latch (is (.contains (.getMessage @e) "Redirect URI does not specify a valid host name: https:///"))) (try (.get (client/get (localhost "/bad-redirect") {:async true :validate-redirects false} (fn [resp] (is false (str "should not have been called but got" resp))) (fn [err] (is false (str "should not have been called but got" err)))) 1 TimeUnit/SECONDS) (is false "should have thrown a timeout exception") (catch TimeoutException te))) (deftest ^:integration test-reusable-http-client (run-server) (let [cm (conn/make-reuseable-async-conn-manager {}) hc (core/build-async-http-client {} cm)] (client/get (localhost "/json") {:connection-manager cm :http-client hc :as :json :async true} (fn [resp] (is (= 200 (:status resp))) (is (= {:foo "bar"} (:body resp))) (is (= hc (:http-client resp)) "http-client is correctly reused")) (fn [e] (is false (str "failed with " e))))) (let [cm (conn/make-reusable-conn-manager {}) hc (:http-client (client/get (localhost "/get") {:connection-manager cm})) resp (client/get (localhost "/json") {:connection-manager cm :http-client hc :as :json})] (is (= 200 (:status resp))) (is (= {:foo "bar"} (:body resp))) (is (= hc (:http-client resp)) "http-client is correctly reused"))) (deftest ^:integration t-cookies-spec (run-server) (try (client/get (localhost "/bad-cookie")) (is false "should have failed") (catch org.apache.http.cookie.MalformedCookieException e)) (client/get (localhost "/bad-cookie") {:decode-cookies false}) (let [validated (atom false) spec-provider (RFC6265CookieSpecProvider.) resp (client/get (localhost "/cookie") {:cookie-spec (fn [http-context] (proxy [org.apache.http.impl.cookie.CookieSpecBase] [] ;; Version and version header (getVersion [] 0) (getVersionHeader [] nil) ;; parse headers into cookie objects (parse [header cookie-origin] (.parse (.create spec-provider http-context) header cookie-origin)) ;; Validate a cookie, throwing MalformedCookieException if the ;; cookies isn't valid (validate [cookie cookie-origin] (reset! validated true)) ;; Determine if a cookie matches the target location (match [cookie cookie-origin] true) ;; Format a list of cookies into a list of headers (formatCookies [cookies] (java.util.ArrayList.))))})] (is (= @validated true)))) (deftest t-cache-config (let [cc (core/build-cache-config {:cache-config {:allow-303-caching true :asynchronous-worker-idle-lifetime-secs 10 :asynchronous-workers-core 2 :asynchronous-workers-max 3 :heuristic-caching-enabled true :heuristic-coefficient 1.5 :heuristic-default-lifetime 12 :max-cache-entries 100 :max-object-size 123 :max-update-retries 3 :revalidation-queue-size 2 :shared-cache false :weak-etag-on-put-delete-allowed true}})] (is (= true (.is303CachingEnabled cc))) (is (= 10 (.getAsynchronousWorkerIdleLifetimeSecs cc))) (is (= 2 (.getAsynchronousWorkersCore cc))) (is (= 3 (.getAsynchronousWorkersMax cc))) (is (= true (.isHeuristicCachingEnabled cc))) (is (= 1.5 (.getHeuristicCoefficient cc))) (is (= 12 (.getHeuristicDefaultLifetime cc))) (is (= 100 (.getMaxCacheEntries cc))) (is (= 123 (.getMaxObjectSize cc))) (is (= 3 (.getMaxUpdateRetries cc))) (is (= 2 (.getRevalidationQueueSize cc))) (is (= false (.isSharedCache cc))) (is (= true (.isWeakETagOnPutDeleteAllowed cc))))) (deftest ^:integration t-client-caching (run-server) (let [cm (conn/make-reusable-conn-manager {}) r1 (client/get (localhost "/get") {:connection-manager cm :cache true}) client (:http-client r1) r2 (client/get (localhost "/get") {:connection-manager cm :http-client client :cache true}) r3 (client/get (localhost "/get") {:connection-manager cm :http-client client :cache true}) r4 (client/get (localhost "/get") {:connection-manager cm :http-client client :cache true})] (is (= :CACHE_MISS (:cached r1))) (is (= :VALIDATED (:cached r2))) (is (= :VALIDATED (:cached r3))) (is (= :VALIDATED (:cached r4)))) (let [cm (conn/make-reusable-conn-manager {}) r1 (client/get (localhost "/dont-cache") {:connection-manager cm :cache true}) client (:http-client r1) r2 (client/get (localhost "/dont-cache") {:connection-manager cm :http-client client :cache true}) r3 (client/get (localhost "/dont-cache") {:connection-manager cm :http-client client :cache true}) r4 (client/get (localhost "/dont-cache") {:connection-manager cm :http-client client :cache true})] (is (= :CACHE_MISS (:cached r1))) (is (= :CACHE_MISS (:cached r2))) (is (= :CACHE_MISS (:cached r3))) (is (= :CACHE_MISS (:cached r4))))) clj-http-3.12.3/test/clj_http/test/headers_test.clj000066400000000000000000000146301407075231000222410ustar00rootroot00000000000000(ns clj-http.test.headers-test (:require [clj-http.client :as client] [clj-http.headers :refer :all] [clj-http.util :refer [lower-case-keys]] [clojure.test :refer :all]) (:import [javax.servlet.http HttpServletRequest HttpServletResponse] [org.eclipse.jetty.server Request Server] org.eclipse.jetty.server.handler.AbstractHandler)) (deftest test-special-case (are [expected given] (is (= expected (special-case given))) nil nil "" "" "foo" "foo" "DNT" "dnt" "P3P" "P3P" "Content-MD5" "content-md5")) (deftest test-canonicalize (are [expected given] (is (= expected (canonicalize given))) nil nil "" "" "Date" :date "Date" :DATE "Foo-Bar-Baz" :foo-bar-baz "Content-MD5" :content-md5)) (deftest test-normalize (are [expected given] (is (= expected (normalize given))) nil nil "" "" "foo" "foo") (is (= "foo" (normalize "foo") (normalize :foo) (normalize "Foo") (normalize :FOO)))) (deftest test-assoc-join (is (= {:foo "1"} (assoc-join {} :foo "1"))) (is (= {:foo "1"} (assoc-join {:foo nil} :foo "1"))) (is (= {:foo ["1" "2"]} (assoc-join {:foo "1"} :foo "2"))) (is (= {:foo ["1" "2" "3"]} (assoc-join {:foo ["1" "2"]} :foo "3")))) (deftest test-header-map (let [m (header-map :foo "bar" "baz" "quux") m2 (assoc m :ham "eggs")] (is (= "bar" (:foo m) (:FOO m) (m :foo) (m "foo") (m "FOO") (get m "foo"))) (is (= {"baz" "quux"} (dissoc m :foo) (dissoc m "foo"))) (is (= #{"Foo" "baz"} (set (keys m)))) (is (= #{"Foo" "Ham" "baz"} (set (keys m2)))) (is (= "eggs" (m2 "ham"))) (is (= "nope" (get m2 "absent" "nope"))) (is (= "baz" (:foo (merge (header-map :foo "bar") {"Foo" "baz"})))) (let [m-with-meta (with-meta m {:withmeta-test true})] (is (= (:withmeta-test (meta m-with-meta)) true))) (testing "select-keys" (are [expected keyset] (= expected (select-keys m keyset)) {"foo" "bar"} ["foo"] {"foo" "bar"} ["foo" "non-existent-key"] {"foo" "bar" "Foo" "bar" :foo "bar"} ["foo" "Foo" :foo])))) (deftest test-empty (testing "an empty header-map is a header-map" (let [m (header-map :foo :bar)] (is (= (class m) (class (empty m))))))) (defn ^Server header-server "fixture server that copies all request headers into the response as response headers" [] ;; argh, we can't use ring for this, because it lowercases headers ;; on the server side, and we explicitly want to get back the ;; headers as they are. so we'll just use jetty directly, nbd. (doto (Server. 18181) (.setHandler (proxy [AbstractHandler] [] (handle [target ^Request base-request ^HttpServletRequest request ^HttpServletResponse response] (.setHandled base-request true) (.setStatus response 200) ;; copy over request headers verbatim (doseq [n (enumeration-seq (.getHeaderNames request))] (doseq [v (enumeration-seq (.getHeaders request n))] ;; (println n v) ;; useful for debugging (.addHeader response n v))) ;; add a response header of our own in known case (.addHeader response "Echo-Server" "Says Hi!") (.. response getWriter (print "Echo!"))))) (.start))) (deftest ^:integration test-wrap-header-map (let [server (header-server)] (try (let [headers {:foo "bar" :etag "some etag" :content-md5 "some md5" :multi ["value1" "value2"] "MySpecialCase" "something"} resp (client/get "http://localhost:18181" {:headers headers}) resp-headers (:headers resp)] (testing "basic sanity checks" (is (= "Echo!" (:body resp))) (is (= "Says Hi!" (:echo-server resp-headers))) ;; was everything copied over correctly (doseq [[k v] headers] (is (= v (resp-headers k))))) (testing "foo is available as a variety of names" (is (= "bar" (:foo resp-headers) (resp-headers "foo") (resp-headers "Foo")))) (testing "header case is preserved" (let [resp-headers (into {} resp-headers)] ;; no more magic (testing "keyword request headers are canonicalized" (is (= "bar" (resp-headers "Foo"))) (is (= "some etag" (resp-headers "ETag"))) (is (= "some md5" (resp-headers "Content-MD5"))) (is (= ["value1" "value2"] (resp-headers "Multi")))) (testing "strings are as written" (is (= "something" (resp-headers "MySpecialCase"))))))) (finally (.stop server))))) (defmacro without-header-map [& body] `(client/with-middleware '~(->> client/default-middleware (list* client/wrap-lower-case-headers) (remove #(= wrap-header-map %)) (vec)) ~@body)) (deftest ^:integration test-dont-wrap-header-map (let [server (header-server)] (try (let [headers {"foo" "bar" "etag" "some etag" "content-md5" "some md5" "multi" ["value1" "value2"] "MySpecialCase" "something"} resp (without-header-map (client/get "http://localhost:18181" {:headers headers})) resp-headers (:headers resp)] (testing "basic sanity checks" (is (= "Echo!" (:body resp))) ;; was everything copied over correctly (doseq [[k v] (lower-case-keys headers)] (is (= v (resp-headers k))))) (testing "header names are all lowercase" (is (= "bar" (resp-headers "foo"))) (is (= "some etag" (resp-headers "etag"))) (is (= "some md5" (resp-headers "content-md5"))) (is (= ["value1" "value2"] (resp-headers "multi"))) (is (= "something" (resp-headers "myspecialcase"))) (is (= "Says Hi!" (resp-headers "echo-server"))))) (finally (.stop server))))) clj-http-3.12.3/test/clj_http/test/links_test.clj000066400000000000000000000030721407075231000217440ustar00rootroot00000000000000(ns clj-http.test.links-test (:require [clj-http.links :refer :all] [clojure.test :refer :all])) (defn- link-handler [link-header] (wrap-links (constantly {:headers {"link" link-header}}))) (deftest test-wrap-links (testing "absolute link" (let [handler (link-handler "; rel=next")] (is (= (:links (handler {})) {:next {:href "http://example.com/page2.html"}})))) (testing "relative link" (let [handler (link-handler ";rel=next")] (is (= (:links (handler {})) {:next {:href "/page2.html"}})))) (testing "extra params" (let [handler (link-handler "; rel=next; title=\"Page 2\"")] (is (= (:links (handler {})) {:next {:href "/page2.html", :title "Page 2"}})))) (testing "multiple headers" (let [handler (link-handler ";rel=prev, ;rel=next,;rel=home")] (is (= (:links (handler {})) {:prev {:href "/p1"} :next {:href "/p3"} :home {:href "/"}})))) (testing "no :links key if no link headers" (let [handler (wrap-links (constantly {:headers {}})) response (handler {})] (is (not (contains? response :links)))))) (deftest t-multiple-link-headers (let [handler (link-handler ["; rel=shorturl" "; rel=icon"]) resp (handler {})] (is (= (:links resp) {:shorturl {:href "http://example.com/Zl_A"} :icon {:href "http://example.com/foo.png"}})))) clj-http-3.12.3/test/clj_http/test/multipart_test.clj000066400000000000000000000175261407075231000226560ustar00rootroot00000000000000(ns clj-http.test.multipart-test (:require [clj-http.multipart :refer :all] [clojure.test :refer :all]) (:import [java.io ByteArrayInputStream ByteArrayOutputStream File] java.nio.charset.Charset [org.apache.http.entity.mime.content ByteArrayBody ContentBody FileBody InputStreamBody StringBody] org.apache.http.util.EntityUtils)) (defn body-str [^StringBody body] (-> body .getReader slurp)) (defn body-bytes [^ContentBody body] (let [buf (ByteArrayOutputStream.)] (.writeTo body buf) (.toByteArray buf))) (defn body-charset [^ContentBody body] (-> body .getContentType .getCharset)) (defn body-mime-type [^ContentBody body] (-> body .getContentType .getMimeType)) (defn make-input-stream [& bytes] (ByteArrayInputStream. (byte-array bytes))) (deftest test-multipart-body (testing "nil content throws exception" (is (thrown-with-msg? Exception #"Multipart content cannot be nil" (make-multipart-body {:content nil})))) (testing "unsupported content type throws exception" (is (thrown-with-msg? Exception #"Unsupported type for multipart content: class java.lang.Object" (make-multipart-body {:content (Object.)})))) (testing "ContentBody content direct usage" (let [contentBody (StringBody. "abc")] (is (identical? contentBody (make-multipart-body {:content contentBody}))))) (testing "StringBody" (testing "can create StringBody with content only" (let [body (make-multipart-body {:content "abc"})] (is (instance? StringBody body)) (is (= "abc" (body-str body))))) (testing "can create StringBody with content and encoding" (let [body (make-multipart-body {:content "abc" :encoding "ascii"})] (is (instance? StringBody body)) (is (= "abc" (body-str body))) (is (= (Charset/forName "ascii") (body-charset body))))) (testing "can create StringBody with content and mime-type and encoding" (let [body (make-multipart-body {:content "abc" :mime-type "stream-body" :encoding "ascii"})] (is (instance? StringBody body)) (is (= "abc" (body-str body))) (is (= (Charset/forName "ascii") (body-charset body))) (is (= "stream-body" (body-mime-type body)))))) (testing "ByteArrayBody" (testing "exception thrown on missing name" (is (thrown-with-msg? Exception #"Multipart byte array body must contain at least :content and :name" (make-multipart-body {:content (byte-array [0 1 2])})))) (testing "can create ByteArrayBody with name only" (let [body (make-multipart-body {:content (byte-array [0 1 2]) :name "testname"})] (is (instance? ByteArrayBody body)) (is (= "testname" (.getFilename body))) (is (= [0 1 2] (vec (body-bytes body)))))) (testing "can create ByteArrayBody with name and mime-type" (let [body (make-multipart-body {:content (byte-array [0 1 2]) :name "testname" :mime-type "byte-body"})] (is (instance? ByteArrayBody body)) (is (= "testname" (.getFilename body))) (is (= "byte-body" (body-mime-type body))) (is (= [0 1 2] (vec (body-bytes body))))))) (testing "InputStreamBody" (testing "exception thrown on missing name" (is (thrown-with-msg? Exception #"Multipart input stream body must contain at least :content and :name" (make-multipart-body {:content (ByteArrayInputStream. (byte-array [0 1 2]))})))) (testing "can create InputStreamBody with name and content" (let [input-stream (make-input-stream 1 2 3) body (make-multipart-body {:content input-stream :name "testname"})] (is (instance? InputStreamBody body)) (is (= "testname" (.getFilename body))) (is (identical? input-stream (.getInputStream body))))) (testing "can create InputStreamBody with name, content and mime-type" (let [input-stream (make-input-stream 1 2 3) body (make-multipart-body {:content input-stream :name "testname" :mime-type "input-stream-body"})] (is (instance? InputStreamBody body)) (is (= "testname" (.getFilename body))) (is (= "input-stream-body" (body-mime-type body))) (is (identical? input-stream (.getInputStream body))))) (testing "can create input InputStreamBody name, content, mime-type and length" (let [input-stream (make-input-stream 1 2 3) body (make-multipart-body {:content input-stream :name "testname" :mime-type "input-stream-body" :length 42})] (is (instance? InputStreamBody body)) (is (= "testname" (.getFilename body))) (is (= "input-stream-body" (body-mime-type body))) (is (identical? input-stream (.getInputStream body))) (is (= 42 (.getContentLength body)))))) (testing "FileBody" (testing "can create FileBody with content only" (let [test-file (File. "testfile") body (make-multipart-body {:content test-file})] (is (instance? FileBody body)) (is (= test-file (.getFile body))))) (testing "can create FileBody with content and mime-type" (let [test-file (File. "testfile") body (make-multipart-body {:content test-file :mime-type "file-body"})] (is (instance? FileBody body)) (is (= "file-body" (body-mime-type body))) (is (= test-file (.getFile body))))) (testing "can create FileBody with content, mime-type and name" (let [test-file (File. "testfile") body (make-multipart-body {:content test-file :mime-type "file-body" :name "testname"})] (is (instance? FileBody body)) (is (= "file-body" (body-mime-type body))) (is (= test-file (.getFile body))) (is (= "testname" (.getFilename body))))) (testing "can create FileBody with content and mime-type and encoding" (let [test-file (File. "testfile") body (make-multipart-body {:content test-file :mime-type "file-body" :encoding "ascii"})] (is (instance? FileBody body)) (is (= "file-body" (body-mime-type body))) (is (= (Charset/forName "ascii") (body-charset body))) (is (= test-file (.getFile body))))) (testing "can create FileBody with content, mime-type, encoding and name" (let [test-file (File. "testfile") body (make-multipart-body {:content test-file :mime-type "file-body" :encoding "ascii" :name "testname"})] (is (instance? FileBody body)) (is (= "file-body" (body-mime-type body))) (is (= (Charset/forName "ascii") (body-charset body))) (is (= test-file (.getFile body) )) (is (= "testname" (.getFilename body))))))) (deftest test-multipart-content-charset (testing "charset is nil if no multipart-charset is supplied" (let [mp-entity (create-multipart-entity [] nil)] (is (nil? (EntityUtils/getContentCharSet mp-entity))))) (testing "charset is set if a multipart-charset is supplied" (let [mp-entity (create-multipart-entity [] {:multipart-charset "UTF-8"})] (is (= "UTF-8" (EntityUtils/getContentCharSet mp-entity)))))) clj-http-3.12.3/test/clj_http/test/util_test.clj000066400000000000000000000051131407075231000215770ustar00rootroot00000000000000(ns clj-http.test.util-test (:require [clj-http.util :refer :all] [clojure.java.io :as io] [clojure.test :refer :all]) (:import org.apache.commons.io.input.NullInputStream org.apache.commons.io.IOUtils)) (deftest test-lower-case-keys (are [map expected] (is (= expected (lower-case-keys map))) nil nil {} {} {"Accept" "application/json"} {"accept" "application/json"} {"X" {"Y" "Z"}} {"x" {"y" "Z"}})) (deftest t-option-retrieval (is (= (opt {:thing? true :thing true} :thing) true)) (is (= (opt {:thing? false :thing true} :thing) false)) (is (= (opt {:thing? false :thing false} :thing) false)) (is (= (opt {:thing? true :thing nil} :thing) true)) (is (= (opt {:thing? nil :thing true} :thing) true)) (is (= (opt {:thing? false :thing nil} :thing) false)) (is (= (opt {:thing? nil :thing false} :thing) false)) (is (= (opt {:thing? nil :thing nil} :thing) nil)) (is (= (opt {:thing? :a :thing nil} :thing) :a))) (deftest test-parse-content-type (are [s expected] (is (= expected (parse-content-type s))) nil nil "" nil "application/json" {:content-type :application/json :content-type-params {}} " application/json " {:content-type :application/json :content-type-params {}} "application/json; charset=UTF-8" {:content-type :application/json :content-type-params {:charset "UTF-8"}} " application/json; charset=UTF-8 " {:content-type :application/json :content-type-params {:charset "UTF-8"}} " application/json; charset=\"utf-8\" " {:content-type :application/json :content-type-params {:charset "utf-8"}} "text/html; charset=ISO-8859-4" {:content-type :text/html :content-type-params {:charset "ISO-8859-4"}})) (deftest test-force-byte-array (testing "empty InputStream returns empty byte-array" (is (= 0 (alength (force-byte-array (NullInputStream. 0)))))) (testing "InputStream contain bytes for JPEG file is coereced properly" (let [jpg-path "test-resources/small.jpg"] ;; coerce to seq to force byte-by-byte comparison (is (= (seq (IOUtils/toByteArray (io/input-stream jpg-path))) (seq (force-byte-array (io/input-stream jpg-path)))))))) (deftest test-gunzip (testing "with input streams" (testing "with empty stream, does not apply gunzip stream" (is (= "" (slurp (gunzip (force-stream (byte-array 0))))))) (testing "with non-empty stream, gunzip decompresses data" (let [data "hello world"] (is (= data (slurp (gunzip (force-stream (gzip (.getBytes data))))))))))) clj-http-3.12.3/test/header-html5-test.html000066400000000000000000000017101407075231000204240ustar00rootroot00000000000000 titletext This is the body clj-http-3.12.3/test/header-test.html000066400000000000000000000021711407075231000173770ustar00rootroot00000000000000 titletext This is the body clj-http-3.12.3/test/jetty-logging.properties000066400000000000000000000002001407075231000211740ustar00rootroot00000000000000# quiet down jetty's logging org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog org.eclipse.jetty.LEVEL=WARN clj-http-3.12.3/test/log4j2.properties000077500000000000000000000010431407075231000175230ustar00rootroot00000000000000status = error dest = err name = PropertiesConfig filter.threshold.type = ThresholdFilter filter.threshold.level = debug appender.console.type = Console appender.console.name = STDOUT appender.console.layout.type = PatternLayout appender.console.layout.pattern = %d | %-5p | [%t] | %c | %m%n rootLogger.level = info rootLogger.appenderRef.stdout.ref = STDOUT # Set this to debug to log all data to/from server # See https://hc.apache.org/httpcomponents-client-4.5.x/logging.html logger.wire.name = org.apache.http.wire logger.wire.level = info