Search code examples
web-servicesscalafinaglefinch

Why Finch using EndPoint to represent Router, Request Parameter and Request Body


In finch, we can define router, request parameters, request body like this.

  case class Test(name: String, age: Int)

  val router: Endpoint[Test] = post("hello") { Ok(Test("name", 30)) }
  val requestBody: Endpoint[Test] = body.as[Test]
  val requestParameters: Endpoint[Test] = Endpoint.derive[Test].fromParams

The benefit is that we can compose EndPoint together. For example, I can define:

The request path is hello and Parameter should have name and age. (router :: requestParameters)

However, I can still run an invalid endpoint which doesnt include any request path successfully (There is actually no compilation error)

Await.ready(Http.serve(":3000", requestParameters.toService)) 

The result is returning 404 not found page. Even though I expect that the error should report earlier like compilation error. I wonder that is this a design drawback or it is actually finch trying to fix ?

Many thanks in advance


Solution

  • First of all, thanks a lot for asking this!

    Let me give you some insight on how Finch's endpoints work. If you speak category theory, an Endpoint is an Applicative embedding StateT represented as something close to Input => Option[(Input, A)].

    Simply speaking, an endpoint takes an Input that wraps an HTTP request and also captures the current path (eg: /foo/bar/baz). When endpoint is applied on to a given request and either matches it (returning Some) or falls over (returning None). When matched, it changes the state of the Input, usually removing the first path segment from it (eg: removing foo from /foo/bar/baz) so the next endpoint is the chain can work with a new Input (and new path).

    Once endpoint is matched, Finch checks if there is something else left in the Input that wasn't matched. If something is left, the match considered unsuccessful and your service returns 404.

    scala> val e = "foo" :: "bar"
    e: io.finch.Endpoint[shapeless.HNil] = foo/bar
    
    scala> e(Input(Request("/foo/bar/baz"))).get._1.path
    res1: Seq[String] = List(baz)
    

    When it comes to endpoints matching/extracting query-string params, no path segments are being touched there and the state is passed to the next endpoint unchanged. So when an endpoint param("foo") is applied, the path is not affected. That simply means, the only way to serve a query-string endpoint (note: an endpoint that only extract query-string params) is to send it a request with empty path /.

    scala> val s = param("foo").toService
    s: com.twitter.finagle.Service[com.twitter.finagle.http.Request,com.twitter.finagle.http.Response] = <function1>
    
    scala> s(Request("/", "foo" -> "bar")).get
    res4: com.twitter.finagle.http.Response = Response("HTTP/1.1 Status(200)")
    
    scala> s(Request("/bar", "foo" -> "bar")).get
    res5: com.twitter.finagle.http.Response = Response("HTTP/1.1 Status(404)")