Skip to content

Filters

Chris Kent edited this page Mar 23, 2018 · 18 revisions

Filters are a way of adding functionality to an API by executing logic before a handler is invoked or after it returns.

When a filter is invoked it is passed the request and a handler. The filter can examine and modify the request before invoking the handler. It can also examine and modify the response returned by the handler. A filter can even decide to skip the handler and immediately return a response itself.

A filter can be applied to all endpoints in an API or it can be applied to a subset of endpoints. Filters can be chained together, with each filter invoking the next filter in the chain until the last filter invokes the handler. A filter is unaware of whether it is invoking another filter or the handler.

Filters are used to implement much of the default behaviour of Osiris, including error handling, response body serialisation and setting the default response headers. These filters can be removed and replaced to change the default behaviour.

Defining a Filter

Filters are defined in the API definition in a similar way to handlers. The main difference between a filter definition and a handler definition is that the filter definition has two parameters - the request and the handler.

This is pretty much the simplest possible filter - it doesn't change the behaviour of the application, it simply logs the request, invokes the handler, logs the response and returns it.

This filter definition does not include a path so the filter is applied to all endpoints.

filter { req, handler ->
    log.debug("request received: {}", req)
    val res = handler(req)
    log.debug("response received: {}", res)
    res
}

Applying a Filter to a Path

This filter is only applied to requests for the endpoint /foo:

filter("/foo") { req, handler ->
    ...
}

Applying a Filter to Multiple Paths

An asterisk in a filter path is a wildcard that matches all values. A wildcard can be used at the end of a path or in the middle.

Using a Wildcard at the end of a Path

If a filter path ends with a wildcard it matches all paths below the specified path down to any depth. The following filter matches the paths /foo, /foo/bar, /foo/bar/baz etc.

filter("/foo/*") { req, handler ->
    ...
}

Using a Wildcard in the Middle of a Path

If a filter path contains a wildcard in the middle, it matches any path segment as the same position in the path. It does not match multiple path segments. This is different from the behaviour if the wildcard is at the end of the path.

This filter matches /foo/123/bar but not /foo/123/abc/bar:

filter("/foo/*/bar") { req, handler ->
    ...
}

Adding Logic to Request Handling

Amending the Request

This filter invokes the handler after adding a custom header to the request:

filter { req, handler ->
    val newReq = req.copy(headers = req.headers + ("Custom-Header" to "foo"))
    handler(newRew)
}

Amending the Response

This filter invokes the handler and amends the response before returning it:

filter { req, handler ->
    val res = handler(req)
    // Change the response body to make it more SHOUTY
    res.copy(body = (res.body as? String)?.toUpperCase())
}

Bypassing the Handler #1

If the user isn't authorised to access the resource this filter throws an exception that is mapped to a status of 403 (forbidden):

filter { req, handler ->
    if (!isAuthorised(req)) throw ForbiddenException()
    handler(req)
}

Bypassing the Handler #2

If the user isn't authorised to access the resource this filter returns a response without invoking the handler:

filter { req, handler ->
    if (isAuthorised(req)) {
        handler(req)
    } else {
        req.responseBuilder()
            .status(403)
            .header(HttpHeaders.CONTENT_TYPE, ContentTypes.TEXT_PLAIN)
            .build("Forbidden")
    }
}

Passing Values to Downstream Filters and Handlers

There are cases where a filter needs to pass a value to other filters or to the handler. For example, a filter might extract the user ID from the authorization token so it can be used in the handler.

This can be done using request attributes. The Request class has a field attributes, a map whose keys are the attribute names (a string) and whose values are the attribute values (with type Any).

A request containing an attribute can be created by calling the withAttribute function on the request passed to the handler. Note, calling this method does not add the attribute the existing request, it creates a new request copied from the original request, but with the attribute added.

A downstream filter or the handler can retrieve the attribute value using the Request.attribute() function or from the Request.attributes map.

For example:

// A filter that extracts the user ID from the auth token
filter { req, handler ->
    val authToken = req.headers[HttpHeaders.AUTHORIZATION]
    val userId = extractUser(authToken)
    val reqWithUserId = req.withAttribute("userId", userId)
    handler(reqWithUserId)
}

// A handler that uses the user ID
get("/foo") { req ->
    val userId = req.attribute<String>("userId")
    // Do something with the user ID
    ...
}

Default Filters

By default, all APIs created with Osiris contain a number of filters that provide some useful behaviour. This includes:

  • Catching exceptions thrown by filters and handlers and returning responses with an error status
  • Serialising objects returned in the response body to JSON
  • Serialising binary data returned into the response body to a Base64 string
  • Setting the default response content type to application/json

These filters are not defined in-line in the API definition. They are specified by assigning a value to the property globalFilters when defining the API. Passing filters to this function allows the default filters to be changed or removed if different behaviour is needed.

Note, globalFilters can only be set in the outermost block defining the API. It cannot be set in a path or auth block.

Removing the Default Filters

When creating the API, assign an empty list to the globalFilters property in the API:

override val api = api<MyComponents> {
   
   globalFilters = listOf()
   
   ...
}

Replacing the Default Filters

Filters that are only useful in one API should be defined in-line in the API definition using a filter block. However, it can be useful to have a standard set of filters that can be reused across multiple APIs.

Filters are defined outside an API definition using the function defineFilter. The syntax is exactly the same as defining a filter in an API definition:

val filter = defineFilter { req, handler ->
   ...
}

Filters defined using defineFilter apply to all endpoints.

The list of the filters should be assigned to the globalFilters property when defining the API:

val myStandardFilters: List<Filter<MyComponents>> = ... 

override val api = api<MyComponents> {

   globalFilters = myStandardFilters

   ...
}

Clone this wiki locally