All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cognitect.aws.http.java.clj Maven / Gradle / Ivy

The newest version!
(ns ^:skip-wiki cognitect.aws.http.java
  (:require [cognitect.aws.http :as aws-http]
            [clojure.core.async :as async]
            [clojure.string :as string])
  (:import [java.io IOException]
           [java.net URI]
           [java.net.http
            HttpClient HttpClient$Redirect HttpRequest HttpRequest$Builder
            HttpRequest$BodyPublishers HttpResponse HttpResponse$BodyHandlers]
           [java.nio ByteBuffer]
           [java.time Duration]
           [java.util.function Function]))

(set! *warn-on-reflection* true)

(defn ^:private dissoc-by
  [pred m]
  (apply dissoc m (filter pred (keys m))))

(def ^:private restricted-headers
  "The headers disallowed by JDK's HttpClient"
  #{:connection :content-length :expect :host :upgrade})

(defmacro java-fn ^Function [argv & body]
  `(reify Function
     (apply [_# x#] ((fn ~argv ~@body) x#))))

(def ^:private method-string
  {:get    "GET"
   :post   "POST"
   :put    "PUT"
   :head   "HEAD"
   :delete "DELETE"
   :patch  "PATCH"})

(defn http-client
  "Create and return a java.net.http.HttpClient with some reasonable defaults"
  []
  (-> (HttpClient/newBuilder)
      (.connectTimeout (Duration/ofMillis 10000))
      (.followRedirects HttpClient$Redirect/NEVER)
      (.build)))

(defn body->body-publisher
  [^ByteBuffer body]
  (if (nil? body)
    (HttpRequest$BodyPublishers/noBody)
    (HttpRequest$BodyPublishers/ofByteArray (.array body))))

(defn request->complete-uri
  "Builds and returns a java.net.URI from the request map."
  [{:keys [scheme server-name server-port uri query-string]
    :or   {scheme "https"}}]
  (let [;; NOTE: we can't use URI's constructor passing individual components, because sometimes
        ;;       the `:uri` part includes query params
        ;;       (e.g. on DeleteObjects op, :uri is `/bucket-name?delete`)
        full-uri (str (name scheme) "://"
                      (aws-http/uri-authority scheme server-name server-port)
                      uri
                      (when query-string (str "?" query-string)))]
    (URI/create full-uri)))

(defn remove-restricted-headers
  "Remove any headers that are disallowed by JDK HttpClient (because HttpClient will determine their
  values itself).

  More info:
    - https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/module-summary.html
    - https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpRequest.Builder.html#header(java.lang.String,java.lang.String)
    - https://www.rfc-editor.org/rfc/rfc7230#section-3.2

  (Not to be confused with the unrelated topic of headers that are restricted from use by developers
  in a browser/XmlHttpRequest context.)"
  [headers]
  (->> headers
       (dissoc-by #(contains? restricted-headers (-> %
                                                     name
                                                     string/lower-case
                                                     keyword)))))

(defn ^:private build-headers
  [java-net-http-request-builder
   {:keys [headers]}]
  (reduce-kv
   (fn [^HttpRequest$Builder req key value]
     (.header req ^String (name key) ^String value))
   java-net-http-request-builder
   (remove-restricted-headers headers)))

(defn ^:private error->category
  "Guess what category thing went wrong based on exception.
  Returns anomaly category.

  This docs contains all possible exception that can be thrown by java.net.http
  https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html"
  [throwable]
  (cond
    (instance? IOException throwable) :cognitect.anomalies/fault

    (instance? IllegalArgumentException throwable) :cognitect.anomalies/incorrect

    (instance? SecurityException throwable) :cognitect.anomalies/forbidden

    :else :cognitect.anomalies/fault))

(defn error->anomaly
  [^Throwable t]
  (if-let [category (error->category t)]
    {:cognitect.anomalies/category category
     :cognitect.anomalies/message  (.getMessage t)
     :cognitect.aws/throwable      t}

    {:cognitect.anomalies/category :cognitect.anomalies/fault
     :cognitect.anomalies/message  (.getMessage t)
     :cognitect.aws/throwable      t}))

(defn request->java-net-http-request
  "Build a java.net.HttpRequest based on an aws client request"
  [{:keys [body request-method timeout-msec]
    :or {timeout-msec 0} :as request}]
  (let [body-publisher (body->body-publisher body)
        uri (request->complete-uri request)
        http-method (method-string request-method)]
    (cond-> (doto (HttpRequest/newBuilder)
              (.method http-method body-publisher)
              (.uri ^java.net.URI uri)
              (build-headers request))
      ;; optionally set read response timeout aka idle timeout
      ;; unset means unbounded
      ;; builder treats any duration less than or equal to zero as illegal argument
      (pos? timeout-msec) (.timeout (Duration/ofMillis timeout-msec))
      :finally (.build))))

(defn format-response-headers
  "Java HTTP Client always return values as arrays. This transforms array of a single item to a single value"
  [^HttpResponse response]
  (into {}
        (map (fn [[key value]] [key (if (> (count value) 1) (vec value) (first value))]))
        (.map (.headers response))))

(defn handle-response
  [^HttpResponse response channel meta]
  (async/put! channel
              {:status  (.statusCode response)
               :body    (ByteBuffer/wrap (.body response))
               :headers (format-response-headers response)
               :meta    meta}))

(defn submit
  [^HttpClient client request channel]
  (try
    (let [java-request (request->java-net-http-request request)
          req          (.sendAsync client java-request (HttpResponse$BodyHandlers/ofByteArray))
          meta         (:meta request)]
      (->
       (.thenApply req
                   (java-fn [response]
                            (handle-response response channel meta)
                            response))
       (.exceptionally (java-fn [ex]
                                (async/put! channel
                                            (error->anomaly ex)))))

      channel)
    (catch Exception ex
      (async/put! channel
                  (error->anomaly ex)))))

(defn stop
  [_]
  "Not implemented")

(defn create
  []
  (let [java-http-client (http-client)]
    (reify aws-http/HttpClient
      (-submit [_ request channel]
        (submit java-http-client request channel))
      (-stop [_]
        (stop java-http-client)))))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy