Skip to content

Rest.li Filters

nishanthshankaran edited this page Oct 21, 2015 · 46 revisions

Contents

Introduction

On the server side, Rest.li provides a mechanism to intercept incoming requests and outgoing responses via filters. Rest.li filters are primarily of two kinds:

  1. Request filters
  2. Response filters

As the name suggests, request filters intercept incoming requests, and response filters intercept outgoing responses. Request filters can be used for a wide range of use cases, including request validation, admission control, and throttling.

Similarly, response filters can be used for a wide range of use cases, including augmentation of response body and encrypting sensitive information in the response payload.

Request Filters

Creating a concrete request filter is simple. All you need to do is implement the com.linkedin.restli.server.filter.RequestFilter interface. This interface has only one method -- onRequest, which is invoked before the actual resource is invoked. The implementation of the onRequest method is free to modify the incoming request and/or reject the incoming request by throwing an exception.

The request filter has access to the FilterRequestContext and NextRequestFilter. FilterRequestContext is an interface that abstracts information regarding the incoming request, including the request URI, projection mask, request query parameters, and request headers. Please see documentation of FilterRequestContext for more info. 'NextRequestFilter' is an interface that provides access to the next filter in the filter chain. Every request filter should trigger the next filter by invoking the 'onRequest' method on the 'NextRequestFilter'. See 'NextRequestFilter' for more info.

Example Request Filter

import com.linkedin.restli.server.filter.FilterRequestContext;
import com.linkedin.restli.server.filter.NextRequestFilter;
import com.linkedin.restli.server.filter.RequestFilter;

public class RestliExampleRequestFilter implements RequestFilter
{
  @Override
  public void onRequest(FilterRequestContext requestContext, NextRequestFilter nextRequestFitler)
  {
    log.debug(String.format("Received %s request for %s resource.", requestContext.getMethodType(),
                                     requestContext.getFilterResourceModel().getResourceName()));
    // Since we done with this filter, trigger the next filter
    nextRequestFilter.onRequest(requestContext);
  }
}

This simple filter print the request type and resource name for every incoming request and passes the request to the next filter in the filter request chain. Once all the filters in the request chain have been successfully invoked, the nextRequestFilter.onRequest(requestContext) invocation by the last filter in the filter chain passes the request to the Rest.li resource.

Response Filters

Creating a concrete response filter is simple. All you need to do is implement the com.linkedin.restli.server.filter.ResponseFilter interface. This interface has only one method -- onResponse, which is invoked after the actual resource is invoked and before the response is handed to the underlying R2 stack. The implementation of the onResponse method can inspect and modify the outgoing response body, HTTP status, and headers. Moreover, the response filter can return an error to the client by throwing an exception in the implementation of the onResponse method.

The response filter has access to the FilterRequestContext, FilterResponseContext and 'NextResponseFilter'. The FilterResponseContext is an interface that abstracts information regarding the outgoing response, including the response HTTP status, response body, and response headers. Please see documentation of FilterResponseContext for more info. 'NextResponseFilter' is an interface that provides access to the next response filter. Every resource filter should trigger the next filter by invoking the 'onResponse' method on the 'NextResponseFilter'. See 'NextResponseFilter' for more info.

Rest.li guarantees that for a given request-response pair, the same instance of FilterRequestContext is made available to both the request filter and response filter.

Example Response Filter

import com.linkedin.restli.server.filter.FilterRequestContext;
import com.linkedin.restli.server.filter.FilterResponseContext;
import com.linkedin.restli.server.filter.NextResponseFilter;
import com.linkedin.restli.server.filter.ResponseFilter;


public class RestliExampleResponseFilter implements ResponseFilter
{
  @Override
  public void onResponse(FilterRequestContext requestContext, FilterResponseContext responseContext,
                         NextResponseFilter nextResponseFilter)
  {
    System.out.println(String.format("Responding to %s request for %s resource with status code %d.", requestContext.getMethodType(),
                                     requestContext.getFilterResourceModel().getResourceName(), responseContext.getResponseEnvelope().getHttpStatus().getCode()));
    // Invoke the next filter.
    nextResponseFilter.onResponse(requestContext, responseContext);
  }
}

This simple filter prints the HTTP response code along with request type and resource name for every outgoing response and hands of the response to the next filter in the chain. Once all the filters in the response chain have been invoked, the nextResponseFilter.onResponse(requestContext, responseContext) invocation by the last filter in the filter chain passes the response to the underlying R2 stack.

Response Envelope API

In prior versions of rest.li response filters, a set of unstructured APIs of several parts of potentially populated results was exposed to filter developers. Based on developer feedback, we decided to change the response API into a more structure manner. FilterResponseContext objects now can access a RestLiResponseEnvelope, and based on the specific type, a filter can acquire a specific subtype with methods to access specific values in the corresponding response type. A typical use case is as follows:

public class RestliExampleResponseFilter implements ResponseFilter
{
  @Override
  public void onResponse(FilterRequestContext requestContext, FilterResponseContext responseContext,
                         NextResponseFilter nextResponseFilter)
  {
    switch (responseContext.getResponseData().getResponseType()) {
      case SINGLE_ENTITY:        // Handle GET, ACTION, and CREATE responses
        RecordResponseEnvelope envelope = responseContext.getResponseData().getRecordResponseEnvelope();
        someMethod(envelope.getStatus());
        anotherMethod(envelope.getRecord());
        envelope.setRecord(new EmptyRecord()); //Modify the response
        break;
      case GET_COLLECTION        // Handles GET_ALL and FINDER responses
        break;
      default:
      // Other types available as well.
    }
    nextResponseFilter.onResponse(requestContext, responseContext);
  }
}

Verifying a Successful Response

It may be helpful for response filters to fail early, if needed, in cases of exceptions returned by the server. See further below on exception handling in response filters for more details.

In such cases, filter writers should verify that the response is not an error before examining the body of a response.

For example, if an exception is thrown by the server, the getEntityResponse() would return null. It would then behoove the filter writer to quickly fail fast if there is an error, otherwise a NullPointerException could arise later on in the response filter. There is one notable exception to this, and that is responses that don't contain data which would require a filter to perform any examination. Such methods include Actions and anything that returns UpdateResponse (Updates and Deletes). Since these responses only contain an HttpStatus and do not contain entities, lists, maps, etc in their responses, it's not possible to run into accidental access of null objects.

Here is an example on how to fail early if there is an error:

 RestLiResponseData responseData = filterResponseContext.getResponseData();
 //Fail fast if the response is an error response.
 if (responseData.isErrorResponse())
 {
   return;
 }

Note that this advice of failing fast due to errors in the response applies only if the filter wants to exclusively deal with successful, happy-path responses by the server. If the filter writer wants to intentionally deal with error responses, they can also use the isErrorResponse() behavior described above to specifically deal with errors.

Configuring Filters

Configuring filters is done via com.linkedin.restli.server.RestLiConfig. RestLiConfig provides a couple of methods to configure request and response filters. These are addRequestFilter, setRequestFilters, addResponseFilter, and setResponseFilters.

Example Java configuration:

    final RestLiConfig config = new RestLiConfig();
    ...
    ...
    // Add request and response filters to the config.
    config.addRequestFilter(new RestLiExampleRequestFilter());
    config.addResponseFilter(new RestLiExampleResponseFilter());
    ...
    ...

Example Spring Configuration:

<bean class="com.linkedin.restli.server.RestLiConfig">
	<property name=“requestFilters>
		<list>
		    <bean class="RestLiExampleRequestFilter”/>
		</list>
	</property>
	<property name=“responseFilters>
		<list>
		    <bean class=“RestLiExampleResponseFilter”/>
		</list>
	</property>
</bean>

When a Rest.li server is configured to use filters, the filters will be invoked for all incoming requests and outgoing responses of all resources hosted by that server. Therefore, when implementing filters, please keep in mind that filters are cross-cutting and should be applicable to all resources that are hosted by the given Rest.li server.

Filter Chaining

Rest.li supports chaining of request filters and response filters. When a Rest.li server is configured to use multiple request filters/response filters, the request/response filters are invoked as per the order specified in the RestLiConfig. All request filters are invoked before the resource implementation is invoked and all response filters are invoked after the resource implementation is invoked.

Approach 1 to chain three request and response filters.

    final RestLiConfig config = new RestLiConfig();
    config.addRequestFilter(new ReqFilterOne(), new ReqFilterTwo(), new ReqFilterThree());
    config.addResponseFilter(new RespFilterOne(), new RespFilterTwo(), new RespFilterThree());

Approach 2 to chain three request and response filters.

    final RestLiConfig config = new RestLiConfig();
    config.addRequestFilter(new ReqFilterOne());
    config.addRequestFilter(new ReqFilterTwo());
    config.addRequestFilter(new ReqFilterThree());
    config.addResponseFilter(new RespFilterOne());
    config.addResponseFilter(new RespFilterTwo());
    config.addResponseFilter(new RespFilterThree());

Approach 3 to chain three request and response filters.

    final RestLiConfig config = new RestLiConfig();
    config.addRequestFilter(Arrays.asList(new ReqFilterOne(), new ReqFilterTwo(), new ReqFilterThree()));
    config.addResponseFilter(Arrays.asList(new RespFilterOne(), new RespFilterTwo(), new RespFilterThree()));

Approach 4 to chain three request and response filters.

<bean class="com.linkedin.restli.server.RestLiConfig">
	<property name=“requestFilters>
		<list>
		    <bean class=“ReqFilterOne”/>
		    <bean class=“ReqFilterTwo”/>
		    <bean class="ReqFilterThree”/>
		</list>
	</property>
	<property name=“responseFilters>
		<list>
		    <bean class=“RespFilterOne”/>
		    <bean class=“RespFilterTwo”/>
		    <bean class=“RespFilterThree”/>
		</list>
	</property>
</bean>

Transferring State Between Filters

It is recommended that Rest.li filters be stateless. To facilitate transfer of state between filters, Rest.li provides a scratch pad in the form of a Java Map. This scratch pad can be accessed via the getFilterScratchpad method on the FilterRequestContext. See below for an example Rest.li filter that computes the request processing time and print it to standard out.

import com.linkedin.RestLi.server.filter.Filter;

public class RestLiExampleFilter implements Filter
{
  private static final String START_TIME = "StartTime";
  @Override
  public void onRequest(FilterRequestContext requestContext, NextRequestFilter nextRequestFitler)
  {
    requestContext.getFilterScratchpad().put(START_TIME, System.nanoTime());
    nextRequestFilter.onRequest(requestContext);
  }
  @Override
  public void onResponse(FilterRequestContext requestContext, FilterResponseContext responseContext,
                         NextResponseFilter nextResponseFilter)
  {
    final Long startTime = (Long) requestContext.getFilterScratchpad().get(START_TIME);
    System.out.println(String.format("Request processing time: %d us", (System.nanoTime() - startTime) / 1000));
    nextResponseFilter.onResponse(requestContext, responseContext);
  }
}

Exception Handling and Filter Chains

The manner in which exceptions are handled in request filters and response filters are different.

Request Filters

If an exception is thrown by a filter that's part of the request filter chain, further processing of the request is terminated and the error handling logic is invoked on the Rest.li callback. In other words, in order for the incoming request to reach the resource implementation, invocation of all request filters need to be successful.

Response Filters

Exception/error handling in the context of response filters is a little more involved than in the case of request filters. Response filters are applied to both successful responses as well as all types of errors.

Such errors can include:

  1. Exceptions thrown by the resource method, including runtime exceptions such as NullPointerException or RestLiServiceException.
  2. Exceptions generated by restli due to bugs in resource methods. These could include bugs such as nulls returned directly from the resource methods, or indirectly such as null values inside of returned objects (e.g a null element list inside of a CollectionResult).

Subsequently, response filters can transform a successful response from the resource to an error response and vice versa. In addition, a successful response from a filter earlier in the filter chain can be transformed into a error response and vice versa by filters that are subsequent in the filter chain.

The exception/error handling behavior of response filters is summarized as follows:

  1. If the last filter in the response filter chain throws an exception, an error response is returned to the client corresponding to this exception.

  2. If an exception is thrown by any filter except the last filter in the filter chain, the exception is included in the RestLiResponseData. The subsequent filters can 'handle/process' this exception by setting an entity/collection/batch response in the RestLiResponseData and changing the HTTP status code in FilterResponseContext.

  3. The response that is generated as a result of executing the response filter chain is the response that is forwarded to the client. Note that the response filter chain can transform a successful/error response from the resource to a error/successful response that's sent to the client.

When a response filter throws an exception, the HTTP status code will be automatically set according to this rule:

  • If the exception is a RestLiServiceException, the status will be taken from the exception.
  • If not, the status will be set to 500 (Internal Server Error).

It is recommended that filters throw a RestLiServiceException.

Also, note that response headers will be reset every time a filter throws an exception.

Making Asynchronous Blocking Calls from Filters

Situations may arise where you may need to make external calls within your filter code. Say for example, there's an external Auth service that your service integrates with. Every call that comes to your service should be first routed to the Auth service for approval, and only if the Auth service give you a green light, can your resource process the request. Let's say you have a RestLi filter that abstracts away the invocation of the Auth service. One way to implement this Auth filter is as follows:

import com.linkedin.restli.server.filter.FilterRequestContext;
import com.linkedin.restli.server.filter.NextRequestFilter;
import com.linkedin.restli.server.filter.RequestFilter;

public class AuthRequestFilter implements RequestFilter
{
  @Override
  public void onRequest(FilterRequestContext requestContext, NextRequestFilter nextRequestFitler)
  {
    String resourceName = requestContext.getResourceModel().getResourceName();
    // Now invoke the auth service.
    Request<Permission> getRequest = builders.get().resourceName(resourceName).build();
    Permission permission = getClient().sendRequest(getRequest).getResponse().getEntity();
    log.debug(String.format("Received permission %s from auth service for request for %s resource.",
                             requestContext.getMethodType(), resourceName));
    if (permission.isGranted()) {
       // Since we have permissions, pass the request along.
       nextRequestFilter.onRequest(requestContext);
    } else {
      throw new RestLiServiceException(HttpStatus.S_401_UNAUTHORIZED, "Permission denied");
    }
  }
}
Clone this wiki locally