An example Clojure CLI HTTP/S client using GraalVM native image
Switch branches/tags
Nothing to show
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src Use spec, expound for option validation Nov 20, 2018
.gitignore More options, rename main namespace Oct 5, 2018
LICENSE Initial commit Oct 4, 2018
README.md Use spec, expound for option validation Nov 20, 2018
deps.edn Use spec, expound for option validation Nov 20, 2018

README.md

Clojurl

An example HTTP/S client CLI using Clojure and GraalVM native image.

Generated with clj.native-cli template. Uses deps.edn and clj.native-image.

Prerequisites

  • GraalVM 1.0.0-RC9 or higher (may also work with RC7 or RC8)
  • Clojure

GraalVM 1.0.0-RC7 added HTTPS as a supported protocol, and this is a brief walkthrough for using it in a Clojure project with GraalVM Community Edition for macOS.

Enable GraalVM HTTPS Support

This details the steps necessary to get HTTPS working with native-image. You can disregard this section if you're using the pre-compiled image, but it may be helpful for compiling this project with native-image or getting HTTPS support working in another project.

  1. Enable HTTPS protocol support with native-image options: --enable-https or --enable-url-protocols=https

  2. Configure path to libsunec.dylib on macOS (or libsunec.do on Linux)

    This shared object comes with the GraalVM distribution and can be found in $GRAALVM_HOME/jre/lib/. GraalVM uses System.loadLibrary to load it at run-time whenever it's first used. The file must either be in the current working directory, or in a path specified in Java system property java.library.path.

    I set the Java system property at run-time, before first HTTPS attempt:

    (System/setProperty "java.library.path"
                        (str (System/getenv "GRAALVM_HOME") "/jre/lib"))

    See this and this for more information on HTTPS support in GraalVM and native images. If you're distributing a native image, you'll need to include libsunec. If it's in the same directory as your image you don't need to set java.library.path.

    You'll see a warning at run-time if this hasn't been properly configured:

    WARNING: The sunec native library could not be loaded.
    
  3. Use more complete certificate store

    GraalVM comes with a smaller set of CA certificates. For reasons they cannot yet distribute the Oracle JDK root certificates. You can workaround this by replacing GraalVM's cacerts. I renamed the file and replaced it with a symbolic link to cacerts from the JRE that comes with macOS Mojave:

    $ mv $GRAALVM_HOME/jre/lib/security/cacerts $GRAALVM_HOME/jre/lib/security/cacerts.bak
    $ ln -s $(/usr/libexec/java_home)/jre/lib/security/cacerts $GRAALVM_HOME/jre/lib/security/cacerts

    If you don't do this, you might see such horrors as this when attempting HTTPS connections:

    Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    8<------------------------
    Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    8<------------------------
    Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    

Usage

Compile the program with GraalVM native-image:

$ clojure -A:native-image

Print CLI options:

$ ./clojurl -h
  -u, --uri URI             URI of request
  -H, --header HEADER       Request header(s)
  -d, --data DATA           Request data
  -m, --method METHOD  GET  Request method e.g. GET, POST, etc.
  -o, --output FORMAT  edn  Output format e.g. edn, hickory
  -v, --verbose             Print verbose info
  -h, --help                Print this message

Responses can be printed in EDN or Hickory format.

Make a request and print response to stdout:

$ ./clojurl -u https://postman-echo.com/get
  {:headers
   {"content-encoding" "gzip",
    "content-type" "application/json; charset=utf-8",
    "date" "Fri, 05 Oct 2018 03:56:49 GMT",
    "etag" "W/\"10b-EZIoyNoyzUvEaPxY+kzMOEgaNh0\"",
    "server" "nginx",
    "vary" "Accept-Encoding",
    "content-length" "194",
    "connection" "keep-alive"},
   :status 200,
   :body
   "{\"args\":{},\"headers\":{\"host\":\"postman-echo.com\",\"accept\":\"text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\",\"accept-encoding\":\"gzip, deflate\",\"user-agent\":\"Java/1.8.0_172\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\"},\"url\":\"https://postman-echo.com/get\"}"}
$ ./clojurl -H Accept=application/json -H X-Session-Id=1234 -H Content-Type=application/json \
     -u https://postman-echo.com/post \
     -m post -d "{'foo':true}"
  {:headers
   {"content-encoding" "gzip",
    "content-type" "application/json; charset=utf-8",
    "date" "Fri, 05 Oct 2018 03:57:06 GMT",
    "etag" "W/\"16d-FiL2opG823uS6YyXMHVrz5k+/Vk\"",
    "server" "nginx",
    "set-cookie"
    "sails.sid=s%3Af-U0lE-XKYPefMu_II_Sggg1HGVI4LlY.lbh1ZWAEX58lBuDVpo2vRZ%2FPAo1AHllJPSPsJ01RFvc; Path=/; HttpOnly",
    "vary" "Accept-Encoding",
    "content-length" "237",
    "connection" "keep-alive"},
   :status 200,
   :body
   "{\"args\":{},\"data\":\"{'foo':true}\",\"files\":{},\"form\":{},\"headers\":{\"host\":\"postman-echo.com\",\"content-length\":\"12\",\"accept\":\"application/json\",\"accept-encoding\":\"gzip, deflate\",\"content-type\":\"application/json\",\"user-agent\":\"Java/1.8.0_172\",\"x-session-id\":\"1234\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\"},\"json\":null,\"url\":\"https://postman-echo.com/post\"}"}

As a proof-of-concept for using Clojure 1.9 + clojure.spec.alpha + Expound with GraalVM native-image, the CLI options are validated using specs and invalid options can be explained using Expound:

$ ./clojurl -u https://postman-echo.com/get -o foo --verbose
Invalid option(s)
-- Spec failed --------------------

  {:headers ...,
   :method ...,
   :output-fn nil,
              ^^^
   :url ...,
   :verbose? ...}

should satisfy

  ifn?

-- Relevant specs -------

:clojurl/output-fn:
  clojure.core/ifn?
:clojurl/options:
  (clojure.spec.alpha/keys
   :req-un
   [:clojurl/url :clojurl/output-fn]
   :opt-un
   [:clojurl/method :clojurl/headers :clojurl/body])

-------------------------
Detected 1 error