Feature Request: Cancellable grpc-web requests
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.
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!
+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 cool, that seems pretty generic; if you'd like to submit a PR to update the output to that, that'd be great. Thanks!
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.
@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.
Is there any follow-up solution to this problem?