HTTP Requests

Each route has a callback which is executed when the route matches a request. Callbacks require explicit input types and Finatra will then try to convert the incoming request into the specified input type. Finatra supports two request types: a Finagle http Request or a custom case class request object.

Finagle c.t.finagle.http.Request

This is a c.t.finagle.http.Request which contains common HTTP attributes.

Custom “request” case class

Custom “request” case classes can be used for declarative parsing of requests with a content-type of application/json with support for type conversions, default values, and validations.

The case class field names should match the request or header parameter name or use the @JsonProperty annotation to specify the JSON field name in the case class (see: example and test case).

A PropertyNamingStrategy can be configured to handle common name substitutions (e.g. snake_case or camelCase). By default, snake_case is used (defaults are set in FinatraJacksonModule).

For example, if we have a POST endpoint which expects a JSON body with a Long “id” and a String “name”, we could define the following case class and Controller route:

case class HiRequest(id: Long, name: String)

...

post("/hi") { hiRequest: HiRequest =>
  "Hello " + hiRequest.name + " with id " + hiRequest.id
}

For the request: POST /hi

{
  "id": 1,
  "name": "Bob"
}

The incoming request body will be parsed as JSON using the configured FinatraObjectMapper into the HiRequest case class. This route would thus return:

statuscode 200:Hello Bob with id 1

More examples in the JSON Integration with Routing section.

For more information on configuring the FinatraObjectMapper see the Jackson Integration section.

Required Fields

Non-option fields without default values are considered required.

If a required field is missing, a CaseClassMappingException is thrown.

Field Annotations

Field annotations specify how to parse a case class field member from the c.t.finagle.http.Request. Supported annotations:


@RouteParam

Denotes a field to be parsed from a named parameter in a given route, e.g.,

case class FromRouteRequest(
  @RouteParam("entity") resource: String,
  @RouteParam id: String)


get("/foo/:entity/:id") { request: FromRouteRequest =>
  s"The resource is ${request.resource} and the id = ${request.id}"
}

Given a request: GET /foo/users/1234

Using FromRouteRequest as an input to the route callback would parse the string “users” into the value of FromRouteRequest#resource and the string “1234” into the value of FromRouteRequest#id.

Thus, this route would respond:

statuscode 200:The resource is users and the id = 1234

Code example.


@QueryParam

Read a value from the request query string by a parameter named for the case class field or by the @QueryParam annotation value.

For example, suppose you want to parse a GET request with three query params: max, startDate, and verbose, e.g.,

GET /users?max=10&start_date=2014-05-30TZ&verbose=true

This can be modeled with the following custom “request” case class which also applies validations:

case class UsersRequest(
  @Max(100) @QueryParam max: Int,
  @PastDate @QueryParam startDate: Option[DateTime],
  @QueryParam verbose: Boolean = false)

get("/users") { request: UsersRequest =>
  ...
}

The max value will be parsed into an Int and validated to be less than or equal to 100. The startDate will be parsed into an Option[DateTime] (meaning it could be omitted without error from the query string) and if present will be validated to be a date in the past. Lastly, the verbose parameter will be parsed into a Boolean type.

You can also set the parameter name as a value in the @QueryParam annotation, e.g.

case class QueryParamRequest(
  @QueryParam foo: String,
  @QueryParam("skip") isSkipped: Boolean)

Using this case class in a route callback for a request:

GET /?foo=bar&skip=false

would parse the string “bar” into the value of QueryParamRequest#foo and parse the string “false” as a Boolean into the QueryParamRequest#isSkipped field.

Code example.


@FormParam

Read a value from a form field with the case class field’s name or as the value specified in the @FormParam annotation from the request body.

Code example.


Request Forwarding

You can forward a request to another controller. This is similar to other frameworks where forwarding will re-use the same request as opposed to issuing a redirect which will force a client to issue a new request.

To forward, you need to include a c.t.finatra.http.request.HttpForward instance in your controller, e.g.,

class MyController @Inject()(
  forward: HttpForward)
  extends Controller {

Then, to use in your route:

get("/foo") { request: Request =>
  forward(request, "/bar")
}

Forwarded requests will bypass the server defined filter chain (as the requests have already passed through the filter chain) but will still pass through controller defined filters.

For example, if a route is defined:

filter[MyAwesomeFilter].get("/bar") { request: Request =>
  "Hello, world."
}

When another controller forwards to this route, MyAwesomeFilter will be executed on the forwarded request.

Multipart Requests

Finatra has support for multi-part requests. Here’s an example of a multi-part POST controller route definition that simply returns all of the keys in the multi-part request:

post("/multipartParamsEcho") { request: Request =>
  RequestUtils.multiParams(request).keys
}

An example of testing this endpoint:

def deserializeRequest(name: String) = {
  val requestBytes = IOUtils.toByteArray(getClass.getResourceAsStream(name))
  HttpCodec.decodeBytesToRequest(requestBytes)
}

"post multipart" in {
  val request = deserializeRequest("/multipart/request-POST-android.bytes")
  request.uri = "/multipartParamsEcho"

  server.httpRequest(
    request = request,
    suppress = true,
    andExpect = Ok,
    withJsonBody = """["banner"]""")
}

JSON Patch Requests

Finatra has support for JSON Patch requests, see JSON Patch definition.

To handle JSON Patch requests, you will first need to register the JsonPatchMessageBodyReader and the JsonPatchExceptionMapper in the server. The JsonPatchMessageBodyReader is for parsing JSON Patch requests as type c.t.finatra.http.jsonpatch.JsonPatch, and JsonPatchExceptionMapper can convert JsonPatchExceptions to HTTP responses.

See Add an ExceptionMapper for more information on exception mappers.

class ExampleServer extends HttpServer {

 override def configureHttp(router: HttpRouter): Unit = {
   router
     .register[JsonPatchMessageBodyReader]
     .exceptionMapper[JsonPatchExceptionMapper]
     .add[ExampleController]
 }
}

Next, you should include a c.t.finatra.http.jsonpatch.JsonPatchOperator instance in your controller, which provides JsonPatchOperator#toJsonNode conversions and support for all JSON Patch operations.

class MyController @Inject()(
  jsonPatchOperator: JsonPatchOperator
) extends Controller {
  ...

}

After the target data has been converted to a JsonNode, just call JsonPatchUtility.operate to apply JSON Patch operations to the target.

For example:

patch("/jsonPatch") { jsonPatch: JsonPatch =>
  val testCase = ExampleCaseClass("world")
  val originalJson = jsonPatchOperator.toJsonNode[ExampleCaseClass](testCase)
  JsonPatchUtility.operate(jsonPatch.patches, jsonPatchOperator, originalJson)
}

An example of testing this endpoint:

"JsonPatch" in {
  val request = RequestBuilder.patch("/jsonPatch")
    .body(
      """[
        |{"op":"add","path":"/fruit","value":"orange"},
        |{"op":"remove","path":"/hello"},
        |{"op":"copy","from":"/fruit","path":"/veggie"},
        |{"op":"replace","path":"/veggie","value":"bean"},
        |{"op":"move","from":"/fruit","path":"/food"},
        |{"op":"test","path":"/food","value":"orange"}
        |]""".stripMargin,
      contentType = Message.ContentTypeJsonPatch)

  server.httpRequestJson[JsonNode](
    request = request,
    andExpect = Ok,
    withJsonBody = """{"food":"orange","veggie":"bean"}""")
}

For more information and examples, see: