What’s coming up in sttp client 3?

Adam Warski
SoftwareMill Tech Blog
7 min readSep 1, 2020

--

For the past three months we’ve been working on an update to sttp client, which is now available as version 3.0.0-RC4.

The main theme of the release is making sttp client simpler, safer and more developer-friendly.

More than ever, sttp client is THE Scala HTTP client you always wanted :)

There’s a couple of breaking changes — take a look at the release notes for the full overview. We’ll cover some of the new features below.

Beskid Żywiecki by Jacek Kunicki

Simpler web sockets

sttp client introduced web socket support in version 2. While the design was comprehensive, it was unnecessarily complex (too flexible!) and irregular. We’re fixing this design flaw by simplifying the process of using web sockets and unifying how they can be used across backends.

Web sockets are now yet another option when describing how the response body should be handled. Response specifications include values such as asString, asByteArray or asJson. This is now supplemented by a couple of asWebSocket variants.

The basic way to use web sockets is through the asWebSocket response description, which takes WebSocket[F] => F[T]function as a parameter. F is the effect type used by the backend, such as Future, IO, Task, ZIO or Identity for synchronous backends, depending on the exact stack that you are using.

The WebSocket trait exposes methods to send and receive messages:

def asWebSocket[F[_], T](f: WebSocket[F] => F[T]): ResponseAs[...]// where:
trait WebSocket[F[_]] {
def receive(): F[WebSocketFrame]
def send(f: WebSocketFrame): F[Unit]
// and other methods
}

These methods can be used to describe the interaction with the web socket. Here’s an example which uses the akka-http backend with Future as the effect type and interacts with the server socket by sending two messages, receiving two messages and finally closing the socket:

The example would look really similar when using the Monix, cats-effect, ZIO or the async-http-client / OkHttp / HttpClient-based backends. Take a look at all of the examples in the source code, which cover some of the combinations mentioned above.

There’s also another possibility of using web sockets, by providing a streaming stage which transforms incoming messages to outgoing messages. This can be akka’s Flow, fs2’s or ZIO’s Stream or Monix’s Observable. For example, using fs2:

Simpler SttpBackend trait

In sttp client, the Request data structure is only a description of the request to be sent, along with request options and a description of how the response should be handled. The majority of the work is delegated to the SttpBackend, which wraps lower-level HTTP clients and can handle a request, basing on its description.

The SttpBackend trait became simpler as well. As web sockets are yet another response specification, no special open-web-socket method is needed. Moreover, the number of type parameters for the backend decreased from 3 to 2.

The second type parameter now specifies not only if the backend uses a specific implementation of non-blocking streams, but can also require certain capabilities from the backend implementation which will be used to send the request, such as support for web sockets or for a specific effect type. The design of backend capabilities is loosely based on ZIO’s environment.

This also means that it should be easier to write wrapper backends, which provide additional functionality such as logging, redirects or request enrichments. Here’s the trait in its current form:

Safe response-as-stream

As an effort to make using sttp client less error-prone and more resource-safe, the default response specification which handles the response as a non-blocking stream now takes a function which should consume that stream:

def asStream[F[_], T, S](s: Streams[S])(f: s.BinaryStream => F[T])
: ResponseAs[Either[String, T], Effect[F] with S]

When the given function completes, sttp will attempt to make sure that the stream is fully consumed and all resources are released.

Note that this is not yet fully implemented (or possible) in all backends.

Moreover, the response specification is parametrised by a stack-specific implementation of streams. Each backend which supports streaming comes with an implementation of Streams. Thanks to that, it’s obvious what the type of the stream should be. For example, here using the ZIO-backed implementation:

There’s still an escape hatch; to get the response as a raw stream there’s the the asStreamUnsafe response specification (along with asWebSocketUnsafe). It is then the responsibility of the client to ensure that the stream/web socket is fully consumed/closed when it’s no longer needed.

Getting raw request bodies

Another improvement to sttp’s response descriptions, suggested by Kasper Kondzielski, is to include an asBoth combinator, which would return the response body handled by both of the given specifications.

In general, this is not always possible, as not each “base” response specification is replayable. For example, if the body is read as a non-blocking stream, it cannot be then replayed (without buffering everything in memory) to create another response. However, if we read the response body as a string and then parse as json, another string or byte-array-based response can be created as well.

This can be useful for logging raw request bodies, but also to compute raw body hashes to verify request integrity. As an example, a description of a response which should be read both as json and as a string would be as follows:

// the response is read into a tuple. Parsing json might fail (1st
// component), but the second is always the raw string.
asBoth(asJson[Fruit], asStringAlways): ResponseAs[(
Either[ResponseException[String, Exception], Fruit],
String
), Any]

For example:

Typed JSON errors

In some cases, the response body should be parsed as json both when the response code is 200, but also if it’s a http-level error (4xx/5xx). This is the main subject of another contribution led by Kasper, to support typed errors.

The asJsonEither[E, B] response specification can return results which have the shape Either[ResponseException[E, JsonError], B]:

  • if the response code is non-2xx, the response body is attempted to be deserialised to E and returned in the left of the either, wrapped in a subclass of ResponseException, HttpError(E).
  • if the response code is 2xx, the response body is attempted to be deserialised to B and returned on the right of the either
  • if either parsing fails, the json-library-specific exception is returned in the left of the either, wrapped in a DeserializationException.

While it might seem that this is a lot of cases, this is a reflection of the fact that an HTTP request might fail in a lot of ways:

  • there might be connection problems, resulting in a transport-layer exception (represented as a failed effect)
  • the request might be successful, but the response code might indicate a client or server error (4xx/5xx)
  • the response might be 2xx, but parsing the response might fail client-side

All of these cases are reified in the response specifications, following the general pattern that errors are represented as either-left values, and successes as either-right.

Errors as failed effects

However, there are also cases where you don’t need that fine-grained error handling, and if anything goes wrong, you just want to have a failed effect. And there’s a simple way to achieve that, using the .getRight combinator that can be used on a response description.

For example, here’s the signature of a response handling specification, which will return a failed effect (or throw an exception in synchronous backends) when the response isn’t 2xx or can’t be parsed:

asJson[Fruit].getRight: ResponseAs[Fruit, Any]

For example:

Testing

sttp client not only provides backends which provide “real” HTTP functionality, but also provides an SttpBackendStub which is very useful for testing. Using it in tests, it’s possible to verify that:

  • the body is properly serialised
  • the requests targets the correct endpoints
  • by providing a “raw” response body (as a byte array/string/raw non-blocking stream) the response is properly deserialised

The backend stub has been extended with support for web sockets; interactions with the server can be stubbed using WebSocketStub (implemented by Lech Głowiak), using which it’s possible to stub the behavior of the server. Any web socket requests for which the web socket stub is returned as a body, both in the “safe” and “unsafe” variants, will yield a WebSocket which behaves according to the specification. For example:

Non-blocking streams can also be tested, however when providing them as a body, they need to be wrapped using RawStream, so that the stub can detect that the value is a low-level stream representation, not a high-level one.

Finally, a RecordingSttpBackend backend wrapper is available, which keeps all requests and corresponding exceptions/responses. This allows inspecting the interactions that happened with the backend after a test run, and verifying that expected requests have been sent.

Debugging

Things not always go according to plan, and the only way out is debugging — either through println, logging or using a “real” debugger, depending on the code at hand and personal taste. Either way, sttp client has two small new features which should make the process slightly easier.

First of all, two main data structures, Request and Response, contain the show(includeBody): String method, which produces a human-friendly representation of the request. This representation is more compact than the default case class .toString, and might hence be more readable. As an example, here’s how a simple request is represented:

basicRequest
.get(uri"https://test.com")
.header(HeaderNames.Authorization, "secret")
.body("1234")
.response(asBoth(asParams, asStringAlways))
.show()
// gives:
GET https://test.com,
response as: (either(as string, as params), as string),
headers: Accept-Encoding: gzip, deflate, Authorization: ***, Content-Type: text/plain; charset=utf-8, Content-Length: 4,
body: string: 1234"

This is in addition to the existing possibility of converting a request to an executable curl command.

Another improvement is that both SttpClientException.ConnectException and SttpClientException.ReadException, into which exceptions thrown by backends are categorised, include the request, which was being sent, along with a meaningful exception message. Small improvement, but might make these stack traces a bit more readable.

New package and organization

sttp client v3 uses a new package name (sttp.client3) and organization (com.softwaremill.sttp.client3). This way sttp client v1, v2 and v3 can be used side-by-side. Make sure to update your imports and build dependencies!

Try it out

sttp client 3.0.0-RC4 is available today. The documentation is still being updated, but thanks to mdoc, all of the code in the docs is compiles and hence is up-to-date.

If you’d have any feedback on the above features, bug reports, improvement suggestions, please let us know by opening an issue. Alternatively, you can write on the gitter channel. Finally, if you have any radical ideas on how to change sttp, now is the time to contribute! :)

--

--

Software engineer, Functional Programming and Scala enthusiast, SoftwareMill co-founder