RSS

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.

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:

  1. Create a ClientReadableStream-wrapper class, and use it to intercept stream events such as the reception of server responses.
  2. 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]});

Feedback

Found a problem with grpc-web or need a feature? File an issue over the grpc-web repository. If you have general questions or comments, then consider posting to the gRPC mailing list or sending us an email at grpc-web-team@google.com.