Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interceptors implementation #766

Closed
stanley-cheung opened this issue Mar 28, 2020 · 2 comments
Closed

Interceptors implementation #766

stanley-cheung opened this issue Mar 28, 2020 · 2 comments

Comments

@stanley-cheung
Copy link
Collaborator

stanley-cheung commented Mar 28, 2020

I just want to give an update here that we are working on the Interceptors implementation and should have something relatively soon.

The overall design we are borrowing elements from #558 and have to take into consideration of our internal use cases as well so there might be some deviation from #558. One of the goals we have is to have a shared implementation of Interceptors both for our internal use case and the open source use case here. I am going to post some more details on the overall design, some examples here soon.

One of the challenges we faced while working on this, is that internally, our codebase is entirely written in Closure - so we can use directives like @interface, @implements to define the Interceptors interface. And generally the user app code which depends on our grpc-web closure library are compiled together with our grpc-web library. One of the issues we faced is that - since we attempt to define the Interceptor class as an @interface in Closure, all the methods got reduced away to something like .a when we compile the library into the npm module using --compilation_level=ADVANCED_OPTIMIZATIONS. There doesn't seem to be a good way for the open source use case here, to be able to write a custom interceptor while it @implements the Interceptor interface we want.

One workaround we found so far is to relax compiling our npm module to be using --compilation_level=SIMPLE_OPTIMIZATIONS instead. This will work - all our "interfaces" classes will survive the compilation, but the drawback is that the index.js file size of the module will increase significantly. We are not sure how much that will affect the final code size after using tools like webpack - we are still looking into that.

This all kind of stems from the fact that Closure and CommonJS has a very different style of exporting a module and we are still trying to figure out what's the best way to workaround this @interface issue while trying to maintain the same implementation both internally and externally (this will really keep us maintainers sane!).

Good news: we figured out how to export interfaces while building the module with ADVANCED_OPTIMIZATIONS, using @export and externs.

So stay tuned - I am going to be posting more development on this in this issue soon. Thanks.

@jmzwcn
Copy link

jmzwcn commented Mar 28, 2020

great

@stanley-cheung
Copy link
Collaborator Author

Here's a high level view of what we are working on - these are still heavily WIP so things might change. We are still working through some issues with integrating with the internal use case, but we felt that this is a good checkpoint to write a summary.


In order to write your own gRPC-Web client interceptor, you will need to implement the following interface:

Interceptor.prototype.intercept = function(request, invoker) {};
  • request is an object that implements the grpc.web.Request interface. It has access to the request proto, the request metadata, the method descriptor and the call options, hopefully everything you need to know about the RPC's request itself.

  • invoker is a function that takes a request and returns an object that implements the ClientReadableStream interface. This is basically the mechanism that we chain to the next interceptor.

  • This .intercept function needs to return an object that implements the ClientReadableStream interface.


So this is an example of how this is all put together.

const TestStreamInterceptor = function() {};                                                                  
TestStreamInterceptor.prototype.intercept = function(request, invoker) {                                      
  const InterceptedStream = function(stream) {                                                                
    this.stream = stream;                                                                                     
  };                                                                                                          
  InterceptedStream.prototype.on = function(eventType, callback) {                                            
    if (eventType == 'data') {                                                                                
      const newCallback = (response) => {                                                                     
        const msg = response.getMessage();                                                                    
        response.setMessage('[Intcpt resp1]' + msg);                                                          
        callback(response);                                                                                   
      };                                                                                                      
      this.stream.on(eventType, newCallback);                                                                 
    } else {                                                                                                  
      this.stream.on(eventType, callback);                                                                    
    }                                                                                                         
    return this;                                                                                              
  };                                                                                                          
  const reqMessage = request.getRequestMessage();                                                             
  const metadata = request.getMetadata();                                                                     
  metadata['Custom-header-1'] = 'value-intercepted-1';                                                        
  reqMessage.setMessage('[Intcpt req1]' + reqMessage.getMessage());                                           
  return new InterceptedStream(invoker(request));                                                             
};  

The overall idea is that - in order to write your own interceptor, first you have to implement the .intercept(request, invoker) method as stated in the section above. In order to implement that function, you essentially have to construct an object (in the example above, we call it InterceptedStream) that implements ClientReadableStream and return it.

In the .intercept() method, you can see that we change the request proto by intercepting request.getRequestMessage() and adding a string [Intcpt req] before the main message payload. We can also do things based on metadata, the method descriptor, etc. Once we are done with messing with the request, we can call invoker(request) to basically call the next interceptor in the chain. Remember, invoker(request) returns a ClientReadableStream. So we save it inside our InterceptedStream and we implement our own on() callback listener. Here we attach an additional string [Intcpt resp] to the response proto. You can also intercept the status callback, the end callback, etc. Once we are satisfied with messing with these callback listeners, we return it up the interceptor chain.


In order to use the interceptors, you add it to the service constructor like this:

var echoService = new EchoServiceClient('http://localhost:8080', null,                     
                                        {'streamInterceptors': [new TestStreamInterceptor()]});

Specifying the interceptor this way will work for both unary call and server streaming calls. We call it StreamInterceptor because actually both unary calls and server streaming calls are implemented as underlying ClientReadableStream. We actually will also have a separate interface for the PromiseInterceptor that will allow us to intercept unaryCall RPCs provided by the PromiseClient variant. The details will probably be covered in another post because we are still working through some issues there.


We understand that this proposal deviates from #558 in some pretty major ways. Part of the reason why we chose to do things this way, is again, trying to have a unified implementation for both the internal use case and the open source use case here. In the internal use case, we have some requirements and have done some work to re-define the API around grpc.web.Request and grpc.web.MethodDescriptor (also related are the GenericClient and UnaryResponse classes). So we want to have the interceptors make use of those interfaces too.

We'd like to hear your feedback on this. We have a partial PR working right now and we are still working through some issues. So let us know what you think!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants