On the server side, Rest.li provides a mechanism to intercept incoming requests and outgoing responses via filters. Each Rest.li filter contains methods that handle both requests and responses.
On the request side, filters can be used for a wide range of use cases, including request validation, admission control, and throttling.
Similarly on the response side, filters can be used for a wide range of use cases, including augmentation of response body and encrypting sensitive information in the response payload.
When using a filter, you have the option of implementing the interface’s onRequest
, onResponse
, and onError
methods - here is where you specify what the filter will do. onRequest is invoked on the request before the actual
resource method is invoked. onResponse
is invoked on the response after the resource method is invoked but before
being passed to the R2 stack. onError
is invoked when an exception occurs in one of the filter’s methods or if it
receives a response error from the previous filter’s onResponse
method. onError
of the first filter in the response
filter chain will also be invoked if the REST resource method returns an error.
If you do not implement these methods, the default behavior for each method is to do nothing. For example, you can choose to only implement the onRequest method. This way, on responses or errors, the filter will simply pass the response/error to the next filter.
When a request arrives, the filters intercept it. If onRequest executes successfully, it will pass it to the next
filter. If an exception occurs, all subsequent filters are skipped, the filter’s onError
will be invoked, and an error
response is passed through the filter chain in reverse and sent back to the client.
When a response is returned from the REST resource method, it is passed into the filter’s onResponse method. If the REST
resource method returns an error response, it will be passed into the onError
method instead. If a filter’s onResponse
executes successfully, it will pass the response to the next filter. If an exception occurs, the filter will pass it to
the next filter’s onError
method.
When onError
is invoked, by default it will pass the error response to the next filter’s onError
method. You can
specify additional handling (e.g. logging the error) before passing the response on. You can specify logic to fix the
error, whereupon the next filter’s onResponse method will be invoked.
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.
Creating a concrete filter is simple. All you need to do is implement the com.linkedin.restli.server.filter.Filter
interface. 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.
Each filter method returns a CompletableFuture<Void>
. A CompletableFuture
represents the status result of filter
execution and has 3 states - completed, completed with exception, and incomplete. The next filter will not be invoked
until the previous filter has completed (either successfully or exceptionally).
If the filter does not call any asynchronous methods, you can simply return CompletableFuture.completedFuture(null)
-
this returns an already completed future, and it will cause the filter chain to invoke the next filter.
If there is an error, you can either throw an Exception or return a future that has already called
future.completeExceptionally(exception)
- both will do the same thing.
If the filter calls an asynchronous method, you can instantiate an incomplete CompletableFuture and return it from the
filter method. This future should be passed into your asynchronous method - when the method finishes, you can call
future.complete(null)
. This will trigger the filter chain to invoke the next filter. If there is an error, you can
call future.completeExceptionally(exception)
. There are more details on this below.
Not completing a future, whether successfully or exceptionally, will cause the filter chain processing to hang indefinitely.
The implementation of the onRequest
method is free to modify the incoming request. Additionally, it can also reject
the incoming request by throwing an exception or completing the future exceptionally - in this case, a response error is
automatically passed into the filter’s onError
method.
The onRequest method has access to the FilterRequestContext
. 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.
After all the filters’ onRequest
method have been successfully invoked, the filter chain passes the request to the
Rest.li resource.
The implementation of the onResponse
method can inspect and modify the outgoing response body, HTTP status, and
headers. Throwing an exception causes the response to be converted into an error response and passed into the next
filter’s onError
method.
The onResponse
method has access to the FilterRequestContext
and FilterResponseContext
. 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.
After the last filter’s onResponse
method has been invoked successfully, the filter chain passes the outgoing response
to the underlying R2 stack. If the last filter’s onResponse
method’s future completes exceptionally, the response is
converted into an error response and is passed into the R2 stack.
The implementation of the onError
method handles errors, and has the capability to alter the response body, HTTP
status, and headers. The onError method has access to the exception that caused the error, FilterRequestContext
, and
FilterResponseContext
.
The CompletableFuture
that is returned by this method should be completed exceptionally unless this filter fixes the
error, whereupon the future should be completed successfully. The paradigm is that if an error exists in the response at
the end of the filter, the future should be completed exceptionally.
If an exception occurs within the onError
method itself, the next filter’s onError
will be invoked. However, the
most recently occurring exception will be passed in as the exception argument.
After the last filter’s onError
method has been invoked, the filter chain passes the outgoing response error to the
underlying R2 stack. If the last filter’s onError method’s future completes successfully (i.e. the error was fixed), the
error response is converted into a success response and passed to the R2 stack.
import com.linkedin.restli.server.filter.FilterRequestContext;
import com.linkedin.restli.server.filter.FilterResponseContext;
import com.linkedin.restli.server.filter.Filter;
public class RestliExampleFilter implements Filter
{
@Override
public CompletableFuture<Void> onRequest(final FilterRequestContext requestContext)
{
log.debug(String.format("Received %s request for %s resource.", requestContext.getMethodType(), requestContext.getFilterResourceModel().getResourceName()));
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Void> onResponse(final FilterRequestContext requestContext, final FilterResponseContext responseContext)
{
System.out.println(String.format("Responding to %s request for %s resource with status code %d.", requestContext.getMethodType(),
requestContext.getFilterResourceModel().getResourceName(), responseContext.getResponseData().getStatus().getCode()));
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Void> onError(Throwable t, final FilterRequestContext requestContext, final FilterResponseContext responseContext)
{
log.debug(t.toString());
CompletableFuture<Void> future = new CompletableFuture<Void>();
if (isErrorFixable(t))
{
fixError();
future.complete(null); // success
}
else
{
future.completeExceptionally(t); // could not fix error, so this filter did not execute successfully
}
return future;
}
}
When a request arrives, this filter prints the request type and resource name for every incoming request.
When a response is sent, this filter prints the HTTP response code along with request type and resource name for every outgoing response.
When there is an error, this filter logs the exception that caused it. Notice how the filter has the ability to either fix the error or propagate the error (complete normally vs. complete exceptionally).
The FilterResponseContext
has access to a RestLiResponseData
object. This object contains the response data as a
RestLiResponseEnvelope
, which also includes the HTTP status (if success) and the error exception (if error). Besides,
it contains headers and cookies, as well as indicators for response type and the resource method.
The RestLiResponseEnvelope
contains the actual data from the response, as well as HTTP status (if success) and the
error exception (if error). For example, a GET response would store the retrieved resource data in the envelope.
If there is an error, the exception will never be null but the data stored inside of RestLiResponseEnvelope
will
always be null (the envelope itself will not be null, only the data inside of it). The opposite is true if there is no
error.
The type of response envelope is based on the Rest.li resource method. For example, a GET response would have data
stored in the GetResponseEnvelope
.
Resource Method | Response Envelope |
---|---|
GET |
GetResponseEnvelope |
CREATE |
CreateResponseEnvelope |
ACTION |
ActionResponseEnvelope |
BATCH_GET |
BatchGetResponseEnvelope |
BATCH_PARTIAL_UPDATE |
BatchPartialUpdateResponseEnvelope |
BATCH_UPDATE |
BatchUpdateResponseEnvelope |
BATCH_DELETE |
BatchDeleteResponseEnvelope |
BATCH_CREATE |
BatchCreateResponseEnvelope |
BATCH_FINDER |
BatchFinderResponseEnvelope |
GET_ALL |
GetAllResponseEnvelope |
FINDER |
FinderResponseEnvelope |
UPDATE |
UpdateResponseEnvelope |
PARTIAL_UPDATE |
PartialUpdateResponseEnvelope |
OPTIONS |
OptionsResponseEnvelope |
DELETE |
DeleteResponseEnvelope |
Response envelopes are grouped together based on ResponseTypes
. Each response type shares the same data format, and
thus use the same getters and setters. A parent response envelope is subclassed by the envelopes in the same
ResponseType group.
For example, GetResponseEnvelope
, ActionResponseEnvelope
, and CreateResponseEnvelope
all store a RecordTemplate
and all use getRecord
and setRecord
as their data access methods. As such, RecordResponseEnvelope
is the parent
envelope for all three. Grouping them together this way reduces code duplication because you can write code for all
envelopes that share the same interface.
Response Type | Parent Response Envelope | Child Response Envelopes |
---|---|---|
SINGLE_ENTITY |
RecordResponseEnvelope |
GetResponseEnvelope , CreateResponseEnvelope , ActionResponseEnvelope |
CREATE_COLLECTION |
N/A - only one envelope falls under this response type, so no need for parent |
BatchCreateResponseEnvelope |
GET_COLLECTION |
CollectionResponseEnvelope |
GetAllResponseEnvelope , FinderResponseEnvelope |
BATCH_COLLECTION |
N/A - only one envelope falls under this response type, so no need for parent |
BatchFinderResponseEnvelope |
BATCH_ENTITIES |
BatchResponseEnvelope |
BatchGetResponseEnvelope , BatchUpdateResponseEnvelope , BatchPartialUpdateResponseEnvelope , BatchDeleteResponseEnvelope |
STATUS_ONLY |
EmptyResponseEnvelope |
PartialUpdateResponseEnvelope , UpdateResponseEnvelope , DeleteResponseEnvelope , OptionsResponseEnvelope |
A typical use case is as follows - notice there are 2 ways to handle different response data types, the first using the resource method and the second using the response type:
public class RestliExampleFilter implements Filter
{
@Override
public CompletableFuture<Void> onResponse(FilterRequestContext requestContext, FilterResponseContext responseContext)
{
RestLiResponseData<?> responseData = responseContext.getResponseData();
switch (responseData.getResourceMethod())
{
// Example showing determining code path based on resource method (CREATE, GET, etc.)
case CREATE: // Handle CREATE response
CreateResponseEnvelope envelope = (CreateResponseEnvelope) responseData.getResponseEnvelope();
someMethod(envelope.getStatus());
anotherMethod(envelope.getRecord());
envelope.setRecord(new EmptyRecord()); //Modify the response
break;
case GET // Handles GET responses
break;
default:
// Other types available as well.
}
// Another example, this time showing determining code path based on response type (SINGLE_ENTITY, GET_COLLECTION, etc.)
switch (responseData.getResponseType())
{
case SINGLE_ENTITY: // Handle GET, ACTION, and CREATE responses - note how you can apply the same logic to all 3 because they share the same data access interface
RecordResponseEnvelope envelope = (RecordResponseEnvelope) responseData.getResponseEnvelope();
someMethod(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.
}
return CompletableFuture.completedFuture(null);
}
}
Rest.li supports chaining of filters. When a Rest.li server is configured to use multiple filters, the filters are
ordered in the same order specified in the RestLiConfig
. On requests, filters that are declared closer to the
beginning are invoked first. On responses, filters that are declared closer to the end are invoked first. See diagram at
top of document for visualization.
Approach 1 to chain three filters.
final RestLiConfig config = new RestLiConfig();
config.addFilter(new FilterOne(), new FilterTwo(), new FilterThree());
Approach 2 to chain three filters.
final RestLiConfig config = new RestLiConfig();
config.addFilter(new FilterOne());
config.addFilter(new FilterTwo());
config.addFilter(new FilterThree());
Approach 3 to chain three filters.
final RestLiConfig config = new RestLiConfig();
config.addFilter(Arrays.asList(new FilterOne(), new FilterTwo(), new FilterThree()));
Approach 4 to chain three filters
<bean class="com.linkedin.restli.server.RestLiConfig">
<property name=“filters>
<list>
<bean class=“FilterOne”/>
<bean class=“FilterTwo”/>
<bean class="FilterThree”/>
</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.
The manner in which exceptions are handled in the filter’s request vs. response methods are different.
There are 2 ways a filter can invoke an exception:
future.completeExceptionally(throwable)
If an exception is thrown while processing a request or if the future is completed exceptionally, further processing of
the request is terminated and the filter’s onError method is invoked. In other words, in order for the incoming request
to reach the resource implementation, invocation of all filters’ onRequest
methods needs 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:
NullPointerException
or
RestLiServiceException
.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 an 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:
onError
method is invoked. You can specify error handling in the onError method
(i.e. fix the error or propagate it to the next filter).When an exception occurs, the HTTP status code will be automatically set according to this rule:
RestLiServiceException
, the status will be taken from the exception.It is recommended that filters throw a RestLiServiceException
.
Note that response headers will be maintained if an exception is thrown, however some new headers signifying an error may be added.
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.Filter;
public class AuthFilter implements Filter
{
@Override
public CompletableFuture<Void> onRequest(FilterRequestContext requestContext)
{
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.
return CompletableFuture.completedFuture(null);
}
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.
The Rest.li filters provide a CompletableFuture interface that handles the asynchronous callbacks for you. The implementation is shown below:
import com.linkedin.restli.server.filter.FilterRequestContext;
import com.linkedin.restli.server.filter.Filter;
public class AuthFilter implements Filter
{
@Override
public CompletableFuture<Void> onRequest(FilterRequestContext requestContext)
{
CompletableFuture<Void> future = new CompletableFuture<Void>();
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.
future.complete(null);
}
else
{
future.completeExceptionally(new RestLiServiceException(HttpStatus.S_401_UNAUTHORIZED, "Permission denied"));
}
}
@Override
public void onError(Throwable e)
{
future.completeExceptionally(new RestLiServiceException(HttpStatus.S_500_INTERNAL_SERVER_ERROR, e));
}
}
// Invoke the auth service asynchronously.
getClient().sendRequest(getRequest, requestContext, cb);
return future;
}
}
The above implementation makes an asynchronous blocking call 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 CompletableFuture
, you can make
outgoing asynchronous calls from within RestLi filters.