Testing and Error Handling in http4s

The missing docs for testing, error, handling, and JSON decoding

Read this if you are trying to get started with http4s, or are looking for a minimal scala web framework. Skip to the heading “http4s testing” if you know all about http4s and the libraries it depends on.

An example project containing all the code used in this tutorial is here.

http4s Is an idiomatic, purely functional HTTP layer for Scala. It pitches itself as being an equivalent to Rack or WSGI, but with its DSL is a lot more like Sinatra or Flask (in my opinion, and not that of the maintainers).

If you want to do web services in Scala without the overhead of an all encompassing framework, it’s a good choice. Scalatra or Finatra aim to be more fully featured. The advantage of http4s is that it is primarily built on the Typelevel and Scalaz library stacks. If you like them, you’ll likely like http4s.

General http4s tutorial

The http4s tutorial is here. It’s good. This tutorial is explicitly based on that tutorial extends its examples, and supplements it. This article provides a very brief introduction to the http4s model in order to make error handling and testing easier to understand

This is the version for 0.15, which is the latest release. Later versions provide a transition path to cats-effect and fs2 to replace Task and scalaz-streams.

The http4s model

The http4s APIs exist on two levels: “plain” objects that describe requests, responses, JSON, and the like, and APIs that return scalaz.concurrent.Task objects that wrap plain objects.

Tasks are like a purer, delayed, future. Because they are trampolined, they don’t need their own thread. You do need to explicitly run them, and you can rerun them over and over (which should be safe if all your code is pure within them; http4s’ code is pure). That’s the important thing: you’re getting a monadic (wrapper) object that needs to be run explicitly.

A good description of the differences is here and the best known documentation is here.

Finally, http4s uses scalaz-streams (or later, FS2, the successor project) for dealing with streams. For our purposes, that means generating and modelling request and response bodies. If Tasks represent a delayed computation, Streams represent a delayed computation of a (possibly infinite) sequence of values. More about that here.

If you’re confused about streams, consider the difference between reducing a sequence, and getting only the final result, and performing a reduce where you incrementally request each reduced value in turn. The former is like a Task; the latter is like a Stream.

The other major scala stream library is Monix.

http4s Testing

The http4s tutorial does not (yet) cover testing. The testing strategy shown below is a simple consequence of the layers of tasks and streams in play.

In short, you construct a request with a body, and run the task to get the underlying request to pass to your service. This gives you a task for a response, which you run, extract the body from, run that (or rather, runLast because you’re working with a Stream), and decode to compare the text with your expectation.

In this way, you can test any body or headers.

http4s Error Handling

Now that we understand the basic model, it’s worth looking at our service again:

Note that in line 4, Ok(Json.obj(“message” -> Json.fromString(s”Hello, ${name}”))) doesn’t create a response, but a Task[Response]. That’s why a for comprehension is used for the multiline examples in the tutorial — to work “inside” the Task monad. See also, the documentation of HttpService.

Accordingly, to handle errors in the request, we need to act monadically. First, we need to ensure that errors are returned as objects, rather than raised as exceptions (which is what the attempt in line 21 is doing). Secondly, we need to handle those exceptions using whatever monadic style we like; I find the use of pattern matching the simplest way to decide what to do.

Note that the nested match is because http4s will wrap the io.circe.DecodingFailure with an org.http4s.InvalidMessageBodyFailure, the latter of which does not have the same diagnostic as the show message of the former.

Custom JSON Decoding

This is as much a circe issue as an http4s issue, but neither of them tell you how to do custom decoding, because you likely won’t need it. If you do need it, you’ll need to become familiar with io.circe.HCursor.