sttp : the Scala HTTP client you always wanted! And now it’s getting even better :)

Over the last two years, we’ve got quite a lot of feedback as to what works, and what doesn’t in sttp . Bumping the major version from 1 to 2 is a good opportunity to fix some of the problems.

There’s quite a lot of breaking changes, however they mostly require mechanical fixes. The priniciple of how HTTP requests are described, and then sent, remains the same.

Broadening the sttp namespace

sttp is a really good name for a Scala-related HTTP library. That’s why we want to broaden its meaning, from “Scala HTTP client API” to “HTTP-related Scala APIs”. This means, sttp will now include:

sttp client (describing and sending requests)

sttp tapir (describing endpoints, which can be then converted to a server, openapi documentation or a client)

sttp model (an upcoming stand-alone project, now a module in the sttp repository)

The last project will be a shared dependency between client & tapir . The model itself is quite simple, far (at least for now) from the fully type-safe HTTP metamodels found in akka-http and http4s.

On the practical side, this change means that:

the group id for sttp client artifacts will now be com.softwaremill.sttp.client , instead of com.softwaremill.sttp

for sttp client artifacts will now be , instead of the package is sttp.client , instead of com.softwaremill.sttp

Another (unplanned) change is renaming the starting request description from sttp to basicRequest . Turns out, it’s not a good idea to name values the same as packages! This caused confusion in the past, but has simply stopped working now when sttp is a top-level package in itself.

This means that the Ammonite quick-start to use sttp client now becomes:

import $ivy.`com.softwaremill.sttp.client::core:2.0.0-M3`

import sttp.client.quick._

basicRequest.get(uri"http://httpbin.org/ip").send()

No functional changes: just renames, to make space for tapir & model . See also GitHub issue 288.

Type-safer model

As already mentioned, the model is now a separate project. But also, some HTTP concepts are now represented as classes:

StatusCode is now a proper type instead of a type alias (a value class)

is now a proper type instead of a type alias (a value class) Header is now a proper type, used instead of a 2-tuple of String s

is now a proper type, used instead of a 2-tuple of s Multipart is renamed to Part and generified

It’s possible that the above will require changes in code, however again, these are mostly mechanical updates.

Response-as changes

One of the main sources of confusion in sttp1 was that when parsing the response body as JSON using e.g. circe, the type of Response.body was Either[String, Either[DeserializationError[io.circe.Error], T]] . This was needed to represent both HTTP server/client errors (response codes 4xx or 5xx ), as well as possible parsing failures.

Another problem was that flexibility in parsing the body in case of an HTTP client/server error response was limited. By default, you could get a byte array or a string. This was somewhat improved with the parseResponseIf mechanism (now removed), but the basic problem remained.

That’s why in sttp2 , there are no assumptions as to how the body is parsed: you get full flexibility, at the expense of slightly more verbose types. The ResponseAs[T] type now describes how to parse the body both in case of an error, and in case of success. If you want to represent errors as an Either , you’ll need to use a ResponseAs[Either[String, T]] , for example (before, the Either was hard-coded, as it didn’t need to be explicitly mentioned in the type).

However, we still have reasonable defaults. The default asString response description, has a type of ResponseAs[Either[String, String], Nothing] , and will return Left if the response code is non- 2xx , and a Right otherwise. There’s also asStringAlways , which always returns a string (has type ResponseAs[String, Nothing] .

For JSON, there’s an improved type hierarchy, which makes it clear that there might be two kinds of errors returned:

def asJson[B]: ResponseAs[Either[ResponseError[io.circe.Error], B], Nothing]

The Either signals if there was an error or not. There might be two types of errors: an HttpError ( 4xx / 5xx response), or a DeserializationError .

Existing code will work the same. Only the types have changed. For example:

val oldRequest: Request[String, Nothing] =

sttp.get(uri"http://example.com") val newRequest: Request[Either[String, String], Nothing] =

basicRequest.get(uri"http://example.com")

Finally, it’s possible to dynamically decide how the body should be parsed, depending on the status code and headers, using the fromMetadata base description. See GitHub issue 284 for details.

Other changes

redirect loops now throw an exception, instead of returning a response with status code 0

all json integrations return an Either[ResponseError, B] , instead of throwing exceptions in case parsing fails (as was the case with json4s/play/spray-json)

, instead of throwing exceptions in case parsing fails (as was the case with json4s/play/spray-json) reduced conflicts with cats, so that importing sttp.client._ doesn’t cause namespace clashes: the Id type alias is renamed to Identity , and MonadError is moved to the monad package

doesn’t cause namespace clashes: the type alias is renamed to , and is moved to the package creating “functional” backends is now wrapped in the corresponding effect type, when creating a backend has side-effects (such as creating a thread pool). This affects cats, zio, monix and scalaz backends

backend.close() now returns a F[Unit] . If F is a lazy wrapper (such as IO / Task ), this might mean that you’ll need to update your code, so that the effect is evaluated!

Probably a couple more changes are coming — stay tuned on twitter and GitHub.

And above all: please share your feedback! Either through comments, GitHub issues, twitter messages or elsewhere. It would be great if you could try sttp2 .