Filtering HTTP Requests

c.t.finatra.http.filters.CommonFilters

Finatra composes – in defined order – several generally useful HTTP Filters into c.t.finatra.http.filters.CommonFilters.

CommonFilters can be added in the same manner as any other Filter. Included Filters:

c.t.finatra.http.filters.StatsFilter

c.t.finatra.http.filters.AccessLoggingFilter

  • provides for “Common Log Format” style logging for HTTP requests and responses.

c.t.finatra.http.filters.HttpResponseFilter

  • an HTTP response conformance Filter which, among other things, ensures Location response headers are properly specified.

c.t.finatra.http.filters.ExceptionMappingFilter

for more information.

Global Filters

Filters by default execute after route matching. Meaning, for a given request the URI path is matched in the routing table before executing any Filter. If you need to be able to run a Filter before route matching, you can add the Filter via HttpRouter#filter[T](beforeRouting = true), this is especially useful if the Filter manually inspects to see if it should apply on a given request URI path that may not exist in the routing table (e.g., is not defined by any controller added to the server).

If you want to apply a Filter or Filters to all added controllers you can do the following:

import DoEverythingModule
import ExampleController
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.routing.HttpRouter
import com.twitter.finatra.http.{Controller, HttpServer}

object ExampleServerMain extends ExampleServer

class ExampleServer extends HttpServer {

  override val modules = Seq(
    DoEverythingModule)

  override def configureHttp(router: HttpRouter) {
    router
      .filter[CommonFilters]
      .add[ExampleController]
  }
}

Note that Filters – much like controller routes – are applied in the order defined. Filters can be added to the HttpRouter by type (as in the example above) or by instance.

For more information see the Finagle User’s Guide section on Filters.

Per-controller Filters

It is also possible to add Filters per controller, using HttpRouter#add[F1 <: HttpFilter, C <: Controller].

These Filters will apply to all routes in the Controller.

import DoEverythingModule
import ExampleController
import ExampleFilter
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.filters.AccessLoggingFilter
import com.twitter.finatra.http.routing.HttpRouter
import com.twitter.finatra.http.{Controller, HttpServer}

object ExampleServerMain extends ExampleServer

class ExampleServer extends HttpServer {

  override val modules = Seq(
    DoEverythingModule)

  override def configureHttp(router: HttpRouter) {
    router
      .add[ExampleFilter, ExampleController]
  }
}

Currently, HttpRouter#add supports in-lining up to ten (10) filters before a Controller. If you need to include more than ten Filters please consider combining them with c.t.finatra.filters.MergedFilter in the same manner as c.t.finatra.http.filters.CommonFilters then using the combined Filter in your call to HttpRouter#add.

In all the above usages, we are applying the Filter by type allowing the framework to instantiate instances of the Filters. However, all of these methods support passing constructed instances.

Per-route Filters

Additionally, you can specify Filters inside of a Controller per-route, e.g.,

class ExampleController @Inject()(
  exampleService: ExampleService
) extends Controller {

  filter[ExampleFilter].get("/ping") { request: Request =>
    "pong"
  }

  filter[ExampleFilter]
    .filter[AnotherExampleFilter]
    .get("/name") { request: Request =>
    response.ok.body("Bob")
  }

  filter(new OtherFilter).post("/foo") { request: Request =>
    exampleService.do(request)
    "bar"
  }
}

As you can see, you can choose to apply the Filter either by type or provide an instance. Note that you can chain Controller#filter calls arbitrarily deep.

Request Scope

Guice supports custom scopes in addition to the defined @Singleton, @SessionScoped, and @RequestScoped scopes. @RequestScoped is often used to allow injection of instances which can change depending on the incoming request (e.g. the currently authenticated User).

Finatra provides a custom implementation of the default Guice @RequestScoped functionality which works across Finagle non-blocking threads. The default Guice @RequestScoped implementation uses ThreadLocals which will not work within the context of a Twitter c.t.util.Future.

Note

Fields added to the Custom Request Scope will remain present in threads launched from a FuturePool.

Adding Classes into the Custom Request Scope

First add a dependency on com.twitter:inject-request-scope (finatra/inject/inject-request-scope).

Then define a module which mixes in the c.t.inject.requestscope.RequestScopeBinding trait. This trait defines #bindRequestScope[T] which will bind the given type to an “unseeded” Provider[T] of the type in the custom “FinagleRequestScope”. E.g.,

import com.twitter.inject.TwitterModule
import com.twitter.inject.requestscope.RequestScopeBinding

object UserModule extends TwitterModule with RequestScopeBinding {

  override def configure(): Unit = {
    bindRequestScope[User]
  }
}

Important

Remember to include this Module in your server’s list of Modules.

You must then “seed” this Provider[T] by obtaining an instance of the FinagleRequestScope and calling #seed[T](instance). For request scoping, you would generally do this in a Filter executed on the request path.

For example, to define a Filter which seeds a User into the “FinagleRequestScope”:

import com.twitter.finagle.{Service, SimpleFilter}
import com.twitter.finagle.http.{Request, Response}
import com.twitter.inject.requestscope.FinagleRequestScope
import com.twitter.util.Future
import javax.inject.{Inject, Singleton}

@Singleton
class UserFilter @Inject()(
  finagleRequestScope: FinagleRequestScope
) extends SimpleFilter[Request, Response] {

  def apply(request: Request, service: Service[Request, Response]): Future[Response] = {
    val userId = parseUserId(request) // User-defined method to parse a "user id" from the request
    val user = User(userId)
    finagleRequestScope.seed[User](user)
    service(request)
  }
}

Next, add the FinagleRequestScopeFilter to your server _above_ the defined Filter which seeds the provided instance.

Note

The FinagleRequestScopeFilter expects “request” and “response” type parameters. In this case, they would be com.twitter.finagle.http.Request and com.twitter.finagle.http.Response.

E.g., for the UserFilter defined above (shown with common Filters in a recommended Filter order):

import com.twitter.finagle.http.{Request, Response}
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.filters.{CommonFilters, LoggingMDCFilter, TraceIdMDCFilter}
import com.twitter.finatra.http.routing.HttpRouter
import com.twitter.inject.requestscope.FinagleRequestScopeFilter

class Server extends HttpServer {
  override def configureHttp(router: HttpRouter) {
    router
      .filter[LoggingMDCFilter[Request, Response]]
      .filter[TraceIdMDCFilter[Request, Response]]
      .filter[CommonFilters]
      .filter[FinagleRequestScopeFilter[Request, Response]]
      .filter[UserFilter]
      .add[MyController]
    }
}

Lastly, wherever you need to access the Request scoped User inject a User or a Provider[User] type.

import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import javax.inject.{Inject, Provider, Singleton}

@Singleton
class MyController @Inject()(
  dao: GroupsDAO,
  user: Provider[User])
  extends Controller {

  get("/") { request: Request =>
    "The incoming user has id " + user.get.id
  }
}

Note

The Provider[User] type must be used when injecting into a Singleton class.

Using c.t.finagle.http.Request#ctx

Above we saw how to seed classes to the Finatra Request Scope using a Provider[T].

However, we recommend not seeding with a request scope Provider[T] but instead using Finagle’s c.t.finagle.http.Request#ctx. Internally, for HTTP, we generally use the Request#ctx over Provider[T] even though we use Guice extensively.

To use the Request#ctx technique, first create a RecordSchema request field, a “context”, and an HTTP Filter which can set the value of the “context”.

The “context” should define a method to retrieve the value from the Request#ctx. Typically, this method is defined in an implicit class which takes a c.t.finagle.http.Request as an argument. Importing the “context” members into scope thus allows for calling the method defined in the implicit class as though it were a method on the HTTP Request object.

For example, a UserContext

import com.twitter.finagle.http.Request

// domain object to set as a RecordSchema field
case class User(id: Long)

// create a context
object UserContext {
  private val UserField = Request.Schema.newField[User]() // provide a default value

  // methods from this implicit will be available on the `Request` when UserContext._ is imported
  implicit class UserContextSyntax(val request: Request) extends AnyVal {
    def user: User = request.ctx(UserField)
  }

  private[twitter] def setUser(request: Request): Unit = {
    val user = User(1) //Parse user from request headers/cookies/etc.
    request.ctx.update(UserField, user)
  }
}

And a Filter which can set the User:

// create a Filter
class UserFilter extends SimpleFilter[Request, Response] {
  override def apply(request: Request, service: Service[Request, Response]): Future[Response] = {
    UserContext.setUser(request)
    service(request)
  }
}

In the above example, the retrieval method defined in the implicit class UserContextSyntax will then be available on the the request when the UserContext._ members are imported:

// import the UserContext members into scope, the method Request#user
// will now be available on the Request object.
import UserContext._

class MyController() extends Controller {
  get("/") { request: Request =>
    "Hi " + request.user.id
  }
}