ts-proto icon indicating copy to clipboard operation
ts-proto copied to clipboard

Feature Request: Cancellable grpc-web requests

Open zakhenry opened this issue 3 years ago • 7 comments

Hello, working with the observable grpc-web interface I noticed that unsubscribing from the observable before the response is delivered does not cancel the request.

This is one of the features I am used to with regular http requests, and is valuable for my use case in that in can cancel a long running & expensive API call because the client application is no longer interested in the result.

It appears that grpc-web does return a cancellation callback so it should be possible to implement.

zakhenry avatar Aug 29 '22 18:08 zakhenry

Hey @zakhenry ; cool, that makes sense. Fwiw I haven't really thought much about what a cancellation API would look like. If you'd like to work up a proposal and PR, that'd be great!

stephenh avatar Sep 03 '22 19:09 stephenh

+1

The unsubscribe function should call close:

   return new Observable((observer) => {
      const grpcRequest = grpc.unary(methodDesc, {
        request,
        host: this.host,
        metadata: maybeCombinedMetadata,
        transport: this.options.transport,
        debug: this.options.debug,
        onEnd: (next) => {
          if (next.status !== 0) {
            const err = new GrpcWebError(
              next.statusMessage,
              next.status,
              next.trailers
            );
            observer.error(err);
          } else {
            observer.next(next.message as any);
            observer.complete();
          }
        },
      });
      
      return () => {
        grpcRequest.close()
      }

    }).pipe(take(1));

netanel-utila avatar Sep 17 '22 18:09 netanel-utila

@netanel-utila cool, that seems pretty generic; if you'd like to submit a PR to update the output to that, that'd be great. Thanks!

stephenh avatar Sep 20 '22 00:09 stephenh

We need to see what we can do about promises. The ultimate solution for me is passing an abort controller:

// user code
const controller = new AbortController();
const signal = controller.signal;

client.listUsers({}, { signal })
// lib code
  unary<T extends UnaryMethodDefinitionish>(
    methodDesc: T,
    _request: any,
    metadata: grpc.Metadata | undefined,
  ): Promise<any> {
    const request = { ..._request, ...methodDesc.requestType };
    const maybeCombinedMetadata = metadata && this.options.metadata
      ? new BrowserHeaders({ ...this.options?.metadata.headersMap, ...metadata?.headersMap })
      : metadata || this.options.metadata;
    return new Promise((resolve, reject) => {
      const req = grpc.unary(methodDesc, {
        request,
        host: this.host,
        metadata: maybeCombinedMetadata,
        transport: this.options.transport,
        debug: this.options.debug,
        onEnd: function (response) {
          if (response.status === grpc.Code.OK) {
            resolve(response.message);
          } else {
            const err = new GrpcWebError(response.statusMessage, response.status, response.trailers);
            reject(err);
          }
        },
      });
      
      metadata.signal.addEventListener('abort', () => req.close())
    });
  }

The issue is that currently the metadata takes BrowserHeaders.

netanel-utila avatar Sep 20 '22 05:09 netanel-utila

Screen Shot 2022-09-20 at 8 25 09

netanel-utila avatar Sep 20 '22 05:09 netanel-utila

@paralin could you please give an update on the current status regarding Observables and their integration with the new abort capabilities?

I think I understand that it is now possible to have a flag to pass an abort signal, but I guess for observable apis this doesn't make a lot of sense because I would assume that was handled internally and simply unsubscribing would cause the rpc to abort.

zakhenry avatar Feb 28 '23 19:02 zakhenry

Is there any follow-up solution to this problem?

ghost avatar Jul 11 '23 06:07 ghost