Interceptors in gRPC-Web
We’re pleased to announce support for interceptors in gRPC-web as of release 1.1.0. While the current design is based on gRPC client interceptors available from other gRPC languages, it also includes gRPC-web specific features that should make interceptors easy to adopt and use alongside modern web frameworks.
Introduction
Similar to other gRPC languages, gRPC-web supports unary and
server-streaming interceptors. For each kind of interceptor, we’ve defined an
interface containing a single intercept()
method:
UnaryInterceptor
StreamInterceptor
This is how the UnaryInterceptor
interface is declared:
/*
* @interface
*/
const UnaryInterceptor = function() {};
/**
* @template REQUEST, RESPONSE
* @param {!Request<REQUEST, RESPONSE>} request
* @param {function(!Request<REQUEST,RESPONSE>):!Promise<!UnaryResponse<RESPONSE>>}
* invoker
* @return {!Promise<!UnaryResponse<RESPONSE>>}
*/
UnaryInterceptor.prototype.intercept = function(request, invoker) {};
The intercept()
method takes two parameters:
- A
request
of type grpc.web.Request - An
invoker
, which performs the actual RPC when invoked
The StreamInterceptor
interface declaration is similar, except that the
invoker
return type is ClientReadableStream
instead of Promise
. For
implementation details, see interceptor.js.
Note
AStreamInterceptor
can be applied to any RPC with a ClientReadableStream
return type, whether it’s a unary or a server-streaming RPC.
What can I do with an interceptor?
An interceptor allows you to do the following:
- Update the original gRPC request before passing it along — for example, you might inject extra information such as auth headers
- Manipulate the behavior of the original invoker function, such as bypassing the call so that you can use a cached result instead
- Update the response before it’s returned to the client
You’ll see some examples next.
Unary interceptor example
The code given below illustrates a unary interceptor that does the following:
- It prepends a string to the gRPC request message before the RPC.
- It prepends a string to the gRPC response message after it’s received.
This simple unary interceptor is defined as a class that implements the
UnaryInterceptor
interface:
/**
* @constructor
* @implements {UnaryInterceptor}
*/
const SimpleUnaryInterceptor = function() {};
/** @override */
SimpleUnaryInterceptor.prototype.intercept = function(request, invoker) {
// Update the request message before the RPC.
const reqMsg = request.getRequestMessage();
reqMsg.setMessage('[Intercept request]' + reqMsg.getMessage());
// After the RPC returns successfully, update the response.
return invoker(request).then((response) => {
// You can also do something with response metadata here.
console.log(response.getMetadata());
// Update the response message.
const responseMsg = response.getResponseMessage();
responseMsg.setMessage('[Intercept response]' + responseMsg.getMessage());
return response;
});
};
Stream interceptor example
More care is needed to intercept server-streamed responses from a
ClientReadableStream
using a StreamInterceptor
. These are the main steps to
follow:
- Create a
ClientReadableStream
-wrapper class, and use it to intercept stream events such as the reception of server responses. - Create a class that implements
StreamInterceptor
and that uses the stream wrapper.
The following sample stream-wrapper class intercepts responses and prepends a string to response messages:
/**
* A ClientReadableStream wrapper.
*
* @template RESPONSE
* @implements {ClientReadableStream}
* @constructor
* @param {!ClientReadableStream<RESPONSE>} stream
*/
const InterceptedStream = function(stream) {
this.stream = stream;
};
/** @override */
InterceptedStream.prototype.on = function(eventType, callback) {
if (eventType == 'data') {
const newCallback = (response) => {
// Update the response message.
const msg = response.getMessage();
response.setMessage('[Intercept response]' + msg);
// Pass along the updated response.
callback(response);
};
// Register the new callback.
this.stream.on(eventType, newCallback);
} else {
// You can also override 'status', 'end', and 'error' eventTypes.
this.stream.on(eventType, callback);
}
return this;
};
/** @override */
InterceptedStream.prototype.cancel = function() {
this.stream.cancel();
return this;
};
The intercept()
method of the sample interceptor returns a wrapped stream:
/**
* @constructor
* @implements {StreamInterceptor}
*/
const TestStreamInterceptor = function() {};
/** @override */
TestStreamInterceptor.prototype.intercept = function(request, invoker) {
return new InterceptedStream(invoker(request));
};
Binding interceptors
By passing an array of interceptor instances using an appropriate option key, you can bind interceptors to a client when the client is instantiated:
const promiseClient = new MyServicePromiseClient(
host, creds, {'unaryInterceptors': [interceptor1, interceptor2, interceptor3]});
const client = new MyServiceClient(
host, creds, {'streamInterceptors': [interceptor1, interceptor2, interceptor3]});