Ramping up on Finch: Avoiding Common Gotchas

While we have been using Scala for awhile at Threat Stack, we haven’t been overly satisfied with the HTTP servers that we have used. So a few months ago, we audited a number of options and ultimately decided to try out Finch. We chose Finch for its readable and idiomatic API for constructing HTTP endpoints, solid performance, and some other bells and whistles. We also brought in Circe to handle our JSON serialization needs. Circe was a clear pick for its solid performance and generic derivation.

As we start to use Finch more and more, I want to document our experiences with it. All frameworks have their own little quirks, and Finch and Circe are no exception. In this post I want to cover some of the gotchas and common errors that developers hit when first starting out with Finch. Fortunately, once you know how to deal with them, they are a breeze to avoid and remediate.

Note: This post is probably best for someone who is just starting out with Finch. It assumes that you’ve read some of the documentation or have started playing with the library.

Greedy Routing

The first thing to be aware of is that Finch routers are greedy. They will attempt to map the first thing that they successfully match. Consider this example and associated curl request:

import com.twitter.finagle.Http
import com.twitter.util.Await
import io.finch._
import io.finch.circe._
import io.circe.generic.auto._

val detectiveEndpoint: Endpoint[Detective] = get("detectives")(DetectiveService.fetch _)
val caseEndpoint: Endpoint[Case] = get("case")(CaseService.fetch _)

val routes =
  "police" :: detectiveEndpoint :+:
  "police" :: caseEndpoint

Await.ready(Http.server.serve(":8080", routes.toService))

// $ curl -X GET http://localhost:8080/police/case

Surprisingly this yields a 404. Intuitively, one would think that the route would just match. However, Finch treats each of these lines as completely separate endpoints that are combined with :+:. As a result, the matcher will attempt to greedily match

"police" :: detectiveEndpoint

and ignore

"police" :: patrolmanEndpoint

We can change the previous set of endpoints and make them one endpoint that will attempt to match the path “police” first and then will further attempt to match against the contents of the two different routes:

val routes =
  "police" :: (
    detectiveEndpoint :+:
    patrolmanEndpoint
  )

Obviously these are very simple routes, but it is actually quite easy for this situation to arise when you have a large number of routes that exist in the same path and are separated into several files or controllers.

Different Future Implementations

Another sticking point when using Finch is that it uses Finagle Futures instead of Scala Futures. While this isn’t a bad thing, it can be confusing if you have pre-existing code that uses Scala Futures.

Fortunately, there are ways to convert between the two versions of futures. There is Twitter Bijection which provides conversions between the two.

While Twitter Bijection works well, it is a heavy dependency if all you need it for is the conversion. You can instead use a recommendation from the Finch Cookbook.

import io.finch._

object CaseServiceClient {
  def detectivesOnCase(caseId: Long): scala.concurrent.Future[List[Detective]]
}

val onTheCase = get("case" :: param("case_id").as[Long])({ id =>
  CaseService.detectivesOnCase(id).asTwitter.map(Ok)
})

Another tactic that will help manage these conversions that we have adopted is to convert any Scala Futures into Twitter Futures as close to the call site as we can. This way we can limit the amount that Scala Futures leak into the application. This works well for us with Finch because a number of the functions on Endpoints allow them to be mapped into Twitter Futures. This strategy also works in the other direction, if for example, you were dealing mainly in Scala Futures, but had a library that produced Twitter Futures.

Missing Encoders

One of the most common compile time errors that you will see when using Finch is when you attempt to convert endpoints to a service without all the necessary Encoders. Missing Encoders will produce this error:

An Endpoint you're trying to convert into a Finagle service is missing one or more encoders.

  Make sure shapeless.:+:[Detective,shapeless.:+:[Case,shapeless.CNil]] is one of the following:    	
  * A com.twitter.finagle.http.Response
  * A value of a type with an io.finch.Encode instance (with the corresponding content-type)
  * A coproduct made up of some combination of the above

       (detectiveEndpoint :+: caseEndpoint).toServiceAs[Application.Json] 

While the error seems intimidating at first, all it means is that Finch hasn’t been given a way to convert your data type into a response.

When I encounter this issue, I take the following steps:

  1. Start by looking at all of the case classes that you’ve recently added to the endpoints.
  2. Do they all have encoders/decoders? If not, add one.
  3. If they do have Encoders defined, are they implicit and imported for the Endpoints to use?

These simple steps always get me to the missing encoder or decoder.

Encoder Implicits

Here is another similar error that you may encounter when using Circe’s Automatic Generic Derivation to create Encoders. If Circe can’t find an Encoder for a field on an Endpoint, you will see an error similar to the following:

[error] could not find implicit value for parameter decoder: io.finch.Decode[com.threatstack.detectives.Detective]
[error]     get("detectives")(DetectiveService.fetch _) :+:

The process for discovering the cause of this error is very similar to the process used with the previous error:

  1. Start by looking at all the case classes that you’ve recently added to the endpoints.
  2. Are any of their fields missing decoders/encoders?
  3. Is it a sealed trait hierarchy? If so, you probably forgot the @JsonCodec annotation and Macro Paradise Compiler plugin.

If it isn’t any of these, it’s time to start digging into the Circe documentation.

Wrapping Up

After running into these issues once or twice, they become easy to avoid because you’ll be aware of them. And even if you do encounter them, you’ll know almost immediately how to deal with them.

All in all, these gotchas are very minor and easy to deal with. Finch’s expressiveness and features more than make up for these minor quirks.