-
Notifications
You must be signed in to change notification settings - Fork 546
Rest.li Filters
- Introduction
- Request Filters
- Response Filters
- Configuring Filters
- Filter Chaining
- Transferring State Between Filters
- Exception Handling and Filter Chains
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:
- Request filters
- 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.
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.
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 prints the request type and resource name for every incoming request and forwards 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 incoming request to the Rest.li resource.
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 response filter **must ** 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.
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 passes the outgoing 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 outgoing response to the underlying R2 stack.
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);
}
}
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 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.
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>
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);
}
}
The manner in which exceptions are handled in request filters and response filters are different.
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.
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:
- Exceptions thrown by the resource method, including runtime exceptions such as NullPointerException or RestLiServiceException.
- 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:
-
If the last filter in the response filter chain throws an exception, an error response is returned to the client corresponding to this exception.
-
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 theRestLiResponseData
and changing the HTTP status code inFilterResponseContext
. -
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.
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");
}
}
}
The above implementation makes a synchronous call to an external auth service to authenticate the incoming request. Although the above implementation is functionally correct, it is not very efficient. Upon close investigation, you'll observe that the request processing thread of your service is now blocked on an outgoing call to the auth service. If the auth service is slow to respond to requests, very soon it's possible that all threads of your service is blocked waiting for response from the auth service.
Another way to implement this filter is to use callbacks provided by the RestLi client. The implementation is shown below:
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();
Callback<Response<Permission>> cb = new Callback<Response<Permission>>()
{
@Override
public void onSuccess(Response<Permission> response)
{
Permission permission = response.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");
}
}
@Override
public void onError(Throwable e)
{
throw new RestLiServiceException(HttpStatus.S_500_INTERNAL_SERVER_ERROR, e);
}
}
// Invoke the auth service asynchronously.
getClient().sendRequest(getRequest, requestContext, cb);
}
}
The above implementation makes an asynchronous blocking calls to the external auth service to authenticate the incoming request. In this implementation, the request processing thread of your service is NOT blocked on an outgoing call to the auth service and is free to process more incoming requests for your service. By using callbacks, you can make outgoing asynchronous calls from within RestLi filers.
Quick Access:
- Tutorials
- Dynamic Discovery
- Request Response API
-
Rest.li User Guide
- Rest.li Server
- Resources
- Resource Types
- Resource Methods
- Rest.li Client
- Projections
- Filters
- Wire Protocol
- Data Schemas
- Data
- Unstructured Data
- Tools
- FAQ