Note: the most up-to-date examples are located in the finatra/examples project.
See examples/http-server for a simple example HTTP Server.
HTTP Server Definition¶
To start, add a dependency on the finatra-http-server library. We also recommend using Logback as your SLF4J implementation. E.g.,
with sbt:
"com.twitter" %% "finatra-http-server" % "24.2.0", "ch.qos.logback" % "logback-classic" % versions.logback,
For more information on logging with Finatra see: Introduction to Logging With Finatra.
Create a new class which extends c.t.finatra.http.HttpServer:
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
object ExampleServerMain extends ExampleServer
class ExampleServer extends HttpServer {
override def configureHttp(router: HttpRouter): Unit = {
???
}
}
Or in Java, extend the c.t.finatra.http.AbstractHttpServer:
import com.twitter.app.Flaggable;
import com.twitter.finatra.http.AbstractHttpServer;
import com.twitter.finatra.http.routing.HttpRouter;
public class ExampleServer extends AbstractHttpServer {
public ExampleServer() {
createFlag(
/* name = */ "magic.number",
/* default = */ 55,
/* help = */ "This is a magic number.",
/* flaggable = */ Flaggable.ofJavaInteger());
}
@Override
public void configureHttp(HttpRouter router) {
...
}
}
Tip
See the server naming conventions which is especially important for Java users.
A more complete example includes adding Modules, a Controller, and Filters. In Scala:
import com.google.inject.Module
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.filters.{LoggingMDCFilter, TraceIdMDCFilter}
import com.twitter.finatra.http.routing.HttpRouter
object ExampleServerMain extends ExampleServer
class ExampleServer extends HttpServer {
override val modules: Seq[Module] = Seq(
ExampleModule)
override def configureHttp(router: HttpRouter): Unit = {
router
.filter[LoggingMDCFilter[Request, Response]]
.filter[TraceIdMDCFilter[Request, Response]]
.filter[ExampleFilter]
.add[ExampleController]
}
}
in Java:
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
import com.twitter.app.Flaggable;
import com.twitter.finatra.http.AbstractHttpServer;
import com.twitter.finatra.http.routing.HttpRouter;
import com.twitter.finatra.http.filters.LoggingMDCFilter;
import com.twitter.finatra.http.filters.StatsFilter;
import com.twitter.finatra.http.filters.TraceIdMDCFilter;
import java.util.Collection;
import java.util.Collections;
import scala.reflect.ManifestFactory;
public class ExampleServer extends AbstractHttpServer {
public ExampleServer() {
createFlag(
/* name = */ "magic.number",
/* default = */ 55,
/* help = */ "This is a magic number.",
/* flaggable = */ Flaggable.ofJavaInteger());
}
@Override
public Collection<Module> javaModules() {
return Collections.singletonList(
ExampleModule$.MODULE$);
}
@Override
public void configureHttp(HttpRouter httpRouter) {
httpRouter
.filter(new TypeLiteral<LoggingMDCFilter<Request, Response>>(){})
.filter(ManifestFactory.classType(TraceIdMDCFilter.class))
.filter(new TypeLiteral<StatsFilter<Request>>() {})
.filter(ExampleFilter.class)
.add(ExampleController.class);
}
}
Tip
Take a look at the server naming conventions which is especially important for Java users.
Adding filters in Java can happen in multiple ways which is somewhat dependent on your tolerance to type erasure if you need to apply Filters that have parameterized types.
Above, we show how you can choose to use the .filter(typeLiteral: TypeLiteral[T]) method from the HttpRouter which will obtain the Filter instance described by the TypeLiteral[T] and append it to the filter chain. There is a version which also accepts a binding annotation which is useful if you need to construct a Filter that is not only parameterized but also discriminated by a binding annotation. This is the recommended way to configure generically typed Filters.
Or you can choose to use the scala.reflect.ManifestFactory to pass the Manifest type for a Filter class. Note in the usage above, the TraceIdMDCFilter has two type params, [Req,Rep]. They are not specified when building the Manifest which amounts to the types being erased. In this case, this is OK since we are a.) appending the instance to a chain which will adapt the types appropriately and b.) we don’t expect to obtain the Filter instance from the injector anywhere else in the code (which would need to be asked for as just TraceIdMDCFilter and not TraceIdMDCFilter[Request, Response]).
Finally, you can always pass the class of the Filter to append. The instance will be obtained from the injector and appended to the filter chain.
Tip
Note: to add Modules to your Java server override the javaModules() method.
Simplistically, a server can be thought of as a collection of controllers composed with filters. Additionally, a server can define modules for providing instances to the object graph and how to map exceptions to HTTP responses.
Naming Convention¶
The Finatra convention for defining a server is to create a Scala object with a name ending in “Main” that extends your server class. The server class can be used in testing as this allows your server to be instantiated multiple times in tests without worrying about static state persisting across test runs in the same JVM.
object ExampleServerMain extends ExampleServer
The static object, e.g., ExampleServerMain, would then contain a static main() method for the server to serve as the application entry point for running the server in all other cases.
Java Naming Convention¶
Similarly, in Java the best practice is to create a separate “main” class which defines a Java main() method that accepts the Java command-line arguments for flag parsing:
public final class ExampleServerMain {
private ExampleServerMain() {
}
public static void main(String[] args) {
new ExampleServer().main(args);
}
}
The ExampleServerMain would be the class used as the application entry point for running the server.
Creating an HTTPS Server¶
Finatra provides a default for the defaultHttpPort of “:8888” which means that Finatra will always attempt to start a non-ssl HTTP server on port 8888 if no other configuration is done. The framework allows for users to specify starting an HTTPS server, either additionally or instead.
An HTTPS server can be started by passing in a value for the -https.port flag or overriding the defaultHttpsPort with a non-empty value. To configure the underlying Finagle c.t.finagle.Http.Server transport correctly, override the configureHttpsServer method in your HttpServer definition. E.g.,
import com.twitter.finagle.Http
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
object ExampleHttpsServerMain extends ExampleHttpsServer
class ExampleHttpsServer extends HttpServer {
override val defaultHttpsPort: String = ":443"
// HTTP server configuration
override def configureHttpServer(server: Http.Server): Http.Server = {
server
.withResponseClassifier(???)
.withMaxInitialLineSize(???)
}
// HTTPS server configuration
override def configureHttpsServer(server: Http.Server): Http.Server = {
server
.withResponseClassifier(???)
.withMaxInitialLineSize(???)
.withTransport.tls(???)
}
override def configureHttp(router: HttpRouter): Unit = {
router
.add[ExampleController]
}
}
For convenience, a Tls trait is provided which encapsulates standard TLS configuration for an HTTPS server. Thus you can also do:
import com.twitter.finagle.Http
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.Tls
import com.twitter.finatra.http.routing.HttpRouter
object ExampleHttpsServerMain extends ExampleHttpsServer
class ExampleHttpsServer
extends HttpServer
with Tls {
override val defaultHttpsPort: String = ":443"
override def configureHttp(router: HttpRouter): Unit = {
router
.add[ExampleController]
}
}
Disabling the Default HTTP Server¶
As mentioned, the above configuration will still attempt to start a non-ssl HTTP server. To disable the non-ssl HTTP server, override the defaultHttpPort value to an empty String (and do not pass a value for the -http.port flag), e.g.,
import com.twitter.finagle.Http
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.Tls
import com.twitter.finatra.http.routing.HttpRouter
object ExampleHttpsServerMain extends ExampleHttpsServer
class ExampleHttpsServer
extends HttpServer
with Tls {
override val defaultHttpPort: String = "" // disable the default HTTP port
override val defaultHttpsPort: String = ":443"
override def configureHttp(router: HttpRouter): Unit = {
router
.add[ExampleController]
}
}
Serving a Finagle Service[Request, Response]¶
You have the option to serve a manually constructed Finagle Service[Request, Response] instead of creating an HTTP Controller and having the framework construct a Service[Request, Response] via the HttpRouter.
To do so, extend the HttpServerTrait (or AbstractHttpServerTrait in Java) and implement the httpService: Service[Request, Response] method:
import com.google.inject.Module
import com.twitter.finagle.Service
import com.twitter.finatra.http.HttpServerTrait
import com.twitter.finatra.http.filters.{LoggingMDCFilter, TraceIdMDCFilter}
object ExampleServerMain extends ExampleServer
class ExampleServer extends HttpServerTrait {
override val modules: Seq[Module] = Seq(
ExampleModule)
// the `Service[Request, Response]` to serve
override def httpService: Service[Request, Response] = ???
}
in Java:
import com.google.inject.Module;
import com.twitter.finagle.Service;
import com.twitter.finagle.Request;
import com.twitter.finagle.Response;
import com.twitter.finatra.http.AbstractHttpServer;
import java.util.Collections;
import java.util.Collection;
public class ExampleServer extends AbstractHttpServer {
@Override
public Collection<Module> javaModules() {
return Collections.singletonList(ExampleModule$.MODULE$);
}
@Override
public Service<Request, Response> httpService() {
// the `Service<Request, Response>` to serve
return null;
}
}
Important
The HttpRouter exposes a DSL for users which the framework uses to construct a filtered Service[Request, Response].
Note, you must choose one or the other: either implement your service with a Controller added via the HttpRouter or serve a manually constructed Finagle Service[Request, Response].
Override Default Behavior¶
Flags¶
Some deployment environments may make it difficult to set Flag values with command line arguments. If this is the case, Finatra’s HttpServer’s core flags can be set from code.
For example, instead of setting the -http.port flag, you can override the following method in your server.
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
class ExampleServer extends HttpServer {
override val defaultHttpPort: String = ":8080"
override def configureHttp(router: HttpRouter): Unit = {
...
}
}
For a list of what flags can be set programmatically, please see the HttpServerTrait class.
Framework Modules¶
The HttpServer provides some base configurations in the form of modules added by default to a server’s object graph. This includes:
the FileResolverModule (see: Working With Files)
the ExceptionManagerModule (see: HTTP Exception Mapping)
an overridable default HttpResponseClassifierModule (see: Server-side Response Classification)
an overridable default AccessLogModule
an overridable default ScalaObjectMapperModule (see: Jackson Integration)
an overridable default ValidatorModule (see: Validation Framework)
an overridable default MessageBodyManagerModule (see: Message Body Components)
As expressed above, some of the modules provided in the HttpServer are overridable. An example use-case would be to provide a custom ScalaObjectMapperModule implementation in place of the default ScalaObjectMapperModule
To do so you would override the protected def jacksonModule in your server with your custom implementation.
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
object MyCustomJacksonModule extends ScalaObjectMapperModule {
override val propertyNamingStrategy: PropertyNamingStrategy =
new PropertyNamingStrategy.KebabCaseStrategy
}
...
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
class ExampleServer extends HttpServer {
override def jacksonModule = MyCustomJacksonModule
override def configureHttp(router: HttpRouter): Unit = {
???
}
}
If your module is defined as a class, you would pass an instance of the class, e.g.,
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule
class MyCustomJacksonModule extends ScalaObjectMapperModule {
override val propertyNamingStrategy: PropertyNamingStrategy =
new PropertyNamingStrategy.KebabCaseStrategy
}
...
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
class ExampleServer extends HttpServer {
override def jacksonModule = new MyCustomJacksonModule
override def configureHttp(router: HttpRouter): Unit = {
???
}
}
Caution
Modules are de-duplicated before being installed to create the Injector. If a Framework Module is also configured in the server’s list of Modules, the Framework Module will be replaced.
Finagle Server Configuration¶
If you want to further configure the underlying Finagle server you can override configureHttpServer (or configureHttpsServer) in your server to specify additional configuration on (or override the default configuration of) the underlying Finagle server.
For example:
import com.twitter.finagle.Http
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
class ExampleServer extends HttpServer {
override def configureHttp(router: HttpRouter): Unit = {
???
}
override def configureHttpServer(server: Http.Server): Http.Server = {
server
.withMaxRequestSize(???)
.withAdmissionControl.concurrencyLimit(
maxConcurrentRequests = ???,
maxWaiters = ???
}
}
For more information on Finagle server configuration see the documentation here; specifically the server documentation here.
Server-side Response Classification¶
The default Response Classifier for HTTP servers is HttpResponseClassifier.ServerErrorsAsFailures, which classifies any HTTP 5xx response code as a failure. To configure server-side Response Classification you could choose to set the classifier directly on the underlying Finagle server by overriding the configureHttpServer (or configureHttpsServer) in your server, e.g.,
override def configureHttpServer(server: Http.Server): Http.Server = {
server.withResponseClassifier(???)
}
However, since the server-side ResponseClassifier could affect code not just at the Finagle level, we actually recommend overriding the specific framework module, HttpResponseClassifierModule instead. This binds an instance of an HttpResponseClassifier to the object graph that is then available to be injected into things like the HTTP StatsFilter for a more accurate reporting of metrics that takes into account server-side response classification.
For example, in your HttpServer you would do:
import com.google.inject.Module
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
class ExampleServer extends HttpServer {
override httpResponseClassifierModule: Module = ???
}
The bound value is also then set on the underlying Finagle server before serving.
Testing¶
For information on testing an HTTP server see the HTTP Server Feature Tests section.