Introducing sttp

Adam Warski

25 Jul 2017.4 minutes read

The Scala ecosystem is rich in http client libraries. However, I always found them lacking in some aspect. That's how sttp started coming into existence.

The main goal of sttp is to provide a simple, type-safe API for defining HTTP requests, sending them and receiving responses. We'll dive into the details later, now let's see how to make a simple request!

First things first; you'll need to add a dependency to your SBT project:

"com.softwaremill.sttp" %% "core" % "1.0.5"

Don't worry, the core library doesn't contain any dependencies. Or, if you have the Ammonite REPL, start a session and paste in:

import $ivy.`com.softwaremill.sttp::core:1.0.5`

Defining requests

Now that we have the core library available on the classpath, we can try sending requests. To conveniently work with sttp, we'll need the following import:

import com.softwaremill.sttp._

The entry point for creating requests is the sttp value (coming from the package we just imported). It is an empty request, with only the Accept-Encoding: gzip, deflate header set. As request descriptions are immutable, all methods which we will use going forward will return a new request description. That way it's trivial to create custom "starting points", which can contain e.g. common headers.

If you use an IDE and try to auto-complete, most of the API should be self-explanatory. Let's define a POST request to http://httpbin.org/post and print the response:

val firstRequest = sttp
  .post(uri"http://httpbin.org/post")
  .body("Hello, world!")

To send a request, we need to have an implicit backend in scope. More on that later, for now let's just run the request using the default backend:

implicit val backend = HttpURLConnectionSttpBackend()
val firstResponse = firstRequest.send()

// firstResponse: Response[String]
println(firstResponse.body)

By default, the response body is read into a String. Try it out, you should see a json string returned by httpbin with echoed request headers and body.

As I mentioned before, there's quite a lot of other things that you can specify on a request. From arbitrary headers, to pre-defined ones such as Cookie, authorization, and various body types. For example, here's a more complicated request:

val secondRequest = sttp
  .cookie("session", "*!@#!@!$")
  .body(file) // of type java.io.File
  .put(uri"http://httpbin.org/put")
  .auth.basic("me", "1234")
  .header("Custom-Header", "Custom-Value")
  .response(asByteArray)

An important thing to note is that the methods creating more and more specialized request descriptions can be called in any order. That is, we can first set the headers, then the body, and finally specify the request method & uri. Or, we can start with the uri, and only later specify the headers. This gives you a lot of flexibility when defining partial requests.

You might be wondering what's the .response(asByteArray) bit. A part of the request specification is what to do with the response. By default, the response is read into a utf-8 string (equivalent to .response(asString("utf-8"))), but it can also be ignored, read into a byte array or returned as an InputStream. What's important, you don't need to remember to "consume the response" unless you explicitly request it, for example, as an input stream.

Response types can also be mapped using .mapResponse(f). The type of the response is one of the type parameters of the request description, which is then propagated to the Response[T] type. That way defining requests which parse a string into e.g. JSON is quite straightforward.

Backends

So far we've concentrated mainly on creating the immutable request description. But to actually send a request, you will need a sttp backend. There's a synchronous backend which uses Java's HttpURLConnection behind the scenes provided out-of-the-box: HttpURLConnectionSttpBackend.

As the request definition and request execution are separated, you don't need the backend until the request is ready to be sent using the send() method, which takes an implicit SttpBackend parameter. That way you can create requests without knowing which backend will be used to send it later!

An alternative backend uses akka-http, and is available in the "com.softwaremill.sttp" %% "akka-http-backend" % "1.0.5" package. Unlike the default one, this backend is fully asynchronous. When sending a request using this backend, the send() method will return a Future[Response[T]].

For example, we can send the same firstRequest using the asynchronous Akka backend, and get a future response:

implicit val backend = AkkaHttpSttpBackend()
val futureFirstResponse = firstRequest.send()

// futureFirstResponse: Future[Response[String]]
futureFirstResponse.foreach(r => println(r.body))

There's also a family of Async Http Client-based backends (with Netty used behind the scenes). There are three variants, each wrapping responses in a different type:

  • async-http-client-backend-future, wrapping responses in scala.conccurent.Future (like the akka-http one)
  • async-http-client-backend-scalaz, wrapping responses in scalaz.concurrent.Task
  • async-http-client-backend-monix, wrapping responses in monix.eval.Task

Hence it's easy to integrate sttp with your vanilla Scala/Scalaz/Monix stack. It's also quite straightforward to define your own Async Http Client-based backend.

Summary

That's it for the basics - next we'll cover the URI interpolator and streaming of requests & responses. In the meantime, you can find some more information on the GitHub.

If you would have some ideas or comments regarding the API design, please let us know! Or if you'd like to contribute, there are some tasks waiting on the project board.

Update: read the next blog about sttp which covers streaming support and the URI interpolator.

Blog Comments powered by Disqus.