HTTP Responses

JSON responses

The simplest way to return a JSON response is to return a case class in your route callback. The default framework behavior is to render the case class as a JSON response E.g.,

case class ExampleCaseClass(
  id: String,
  description: String,
  longValue: Long,
  boolValue: Boolean)

get("/foo") { request: Request =>
  ExampleCaseClass(
    id = "123",
    description = "This is a JSON response body",
    longValue = 1L,
    boolValue = true)
}

A request to GET /foo will produce a response:

[Status]  Status(200)
[Header]  Content-Type -> application/json; charset=utf-8
[Header]  Server -> Finatra
[Header]  Date -> Wed, 17 Aug 2015 21:54:25 GMT
[Header]  Content-Length -> 90
{
  "id" : "123",
  "description" : "This is a JSON response body",
  "long_value" : 1,
  "bool_value" : true
}

Note: If you change the default MessageBodyWriter implementation (used by the MessageBodyManager) this will no longer be the default behavior, depending.

You can also always use the c.t.finatra.http.response.ResponseBuilder to explicitly render a JSON response.

Future Conversion

For the basics of Futures in Finatra, see: Futures in the Getting Started documentation.

Conversion Rules

Finatra will attempt to convert your route callback’s return type into a c.t.util.Future[Response] using the following rules:

  • If you return a Future[Response] no conversion will be performed.
  • If you return a Future[T] it will be mapped to wrap the T into an HTTP 200 OK Response, with T as the body.
  • If you return a scala.concurrent.Future[T] a Bijection will be attempted to convert the Scala Future into a Future[T] then the above case is performed.
  • Some[T] will be converted into a HTTP 200 OK.
  • None will be converted into a HTTP 404 NotFound.
  • Non-Response classes will be converted into an HTTP 200 OK with the class written as the response body.

Non-Future Callback Return Types

Callbacks that do not return a c.t.util.Future will have their return values wrapped in a c.t.util.ConstFuture (which does no asynchronous work) for the purpose of satisfying types only.

Don’t Block (No, Seriously, Do Not Block)

If you are not returning a Future from your callback and it does synchronous work or calls a blocking method, you must avoid blocking the Finagle thread by wrapping your blocking operation in a FuturePool e.g.

import com.twitter.finatra.utils.FuturePools

class MyController extends Controller {

  private val futurePool = FuturePools.unboundedPool("CallbackConverter")

  get("/") { request: Request =>
    futurePool {
      blockingOperation()
    }
  }
}

More information on blocking in Finagle can be found here.

c.t.finatra.http.response.ResponseBuilder

All HTTP Controllers have a protected response field of type c.t.finatra.http.response.ResponseBuilder which can be used to build a c.t.finagle.http.Response in your Controller route callback functions.

For example:

get("/foo") { request: Request =>
  ...

  response.
    ok.
    header("a", "b").
    json("""
    {
      "name": "Bob",
      "age": 19
    }
    """)
}

get("/foo") { request: Request =>
  ...

  response.
    status(999).
    body(bytes)
}

get("/redirect") { request: Request =>
  ...

  response
    .temporaryRedirect
    .location("/foo/123")
}

get("/foo/future") { request: Request =>
  ...

  val futureOpResult: Future[Bar] = ...
  futureOpResult.map { result =>
    response
      .ok
      .body(result)
  }
}

post("/users") { request: MyPostRequest =>
  ...

  response
    .created
    .location("/users/123")
}

For more examples, see the ResponseBuilderTest.

Wait, how do I create a Response from a Future[T]?

As noted in the Future Conversion section, Finatra will attempt to construct a proper return type of Future[Response] from your callback’s return type. Though, in many cases, you may find that you have a Future[T] and want to translate this into a c.t.finagle.http.Response yourself using the c.t.finatra.http.response.ResponseBuilder.

Constructing a response is synchronous, thus the c.t.finatra.http.response.ResponseBuilder has no concept of Futures. However, the c.t.finatra.http.response.ResponseBuilder is meant to be somewhat generic so its API for constructing a response body accepts an Any type which may make it seem like it should work to simply put in a Future[T] into the body. However, this is incorrect.

If you have a Future[T] and want to return a c.t.finagle.http.Response you should either:

  • convert it to a Future[Response] or
  • do nothing and let the Finatra CallbackConverter convert the Future[T] to an HTTP 200 OK with T as the body (as mentioned in Future Conversion section above).

To convert a Future[T] to a Future[Response], you would use Future#map:

get("/foo") { request: Request =>
  val futureResult: Future[Foo] = ... // a call that returns a Future[Foo]

  // map the Future[T] to create a Future[Response]
  futureResult.map { result: Foo =>
    // construct your response here using the ResponseBuilder
    response.ok.body(result)
  }
}

Cookies:

Cookies, like Headers, are read from request and can set on the response via the c.t.finatra.http.response.ResponseBuilder:

get("/") { request =>
  val loggedIn = request.cookies.getValue("loggedIn").getOrElse("false")
  response.ok.
    plain("logged in?:" + loggedIn)
}
get("/") { request =>
  response.ok.
    plain("hi").
    cookie("loggedIn", "true")
}

Advanced cookies are supported by creating and configuring c.t.finagle.http.Cookie objects:

get("/") { request =>
  val c = new Cookie(name = "Biz", value = "Baz")
  c.setSecure(true)
  response.ok.
    plain("get:path").
    cookie(c)
}

Response Exceptions:

Responses can be embedded inside exceptions with .toException. You can throw the exception to terminate control flow, or wrap it inside a Future.exception to return a failed Future. However, instead of directly returning error responses in this manner, a better convention is to handle application-specific exceptions in an ExceptionMapper.

get("/NotFound") { request: Request =>
  response.notFound("abc not found").toFutureException
}

get("/ServerError") { request: Request =>
  response.internalServerError.toFutureException
}

get("/ServiceUnavailable") { request: Request =>
  // can throw a raw exception too
  throw response.serviceUnavailable.toException
}

Setting the Response Location Header:

c.t.finatra.http.response.ResponseBuilder has a “location” method.

post("/users") { request: Request =>
  response
    .created
    .location("/users/123")
}

which can be used:

  • if the URI has a valid scheme then the URI is placed in the Location header unchanged.
  • response.location(“/users/123”) will get turned into the correct full URI by the HttpResponseFilter (e.g. https://host.com/users/123) according to rules defined in the URI specification
  • The scheme is assumed to be http when rewritten unless otherwise specified in the “x-forwarded-proto” header in the request.