Generate gRPC request validation middleware
gRPC middleware that validates requests can be generated by embracing protoc-gen-validate. We generate such middleware in the company I work for and works well. It would be great if protoc-gen-validate has an option to generate such middleware.
Thank you.
For which languages? The java implementation comes with gRPC interceptors already implemented.
For which languages?
Go.
The java implementation comes with gRPC interceptors already implemented.
Nice 👍
Howdy! Thanks for writing in. For Go, generation isn't necessary to create validation interceptors since interfaces are implicit:
type Validator interface {
Validate() error
}
// unary
func ServerValidationUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if r, ok := req.(Validator); ok {
if err := r.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
}
return handler(ctx, req)
}
A similar pattern can be written for Server streaming as well as the two client interceptors.
Not including interceptors was also intentional as the behavior required for validation can be complex and very dependent on the service, its callers, or the expected behavior of the input. It also breaks some of the backwards-compatibility behavior inherit to protos if the validation rules ever change. Here are a couple use cases to demonstrate this:
Suppose you have mobile clients making requests with one set of validations. Once these clients are in the wild, you cannot change their behavior until the user updates their app. Now, if you change the validation rules in a way that invalidates requests from those versions of the client, you've essentially broken your API in a backward-incompatible way. In this case, at Lyft, we recommend clients perform validation with the version of the rules they know, while servers run the validations, but do not block "invalid" requests in production. In development/staging, it should be a hard InvalidArgument error though.
For streaming (or unary requests that are written to fail open on errors in the request), returning an error as you do with unary will stop the rest of the stream, instead of just responding to the single bad request. That may or may not be the desired behavior, but chances are it's not. For the fail-open scenario above, suppose the request contains a field of type repeated SubRequest, where you want each SubRequest to be validated independently. Doing it globally at an interceptor does not permit this behavior.
Thank you for your detailed response!
The type assertion code satisfies my original request/question. Originally we wanted to avoid type assertion but that should be enough.
I found the mobile app example interesting. How do you guys keep multiple validations for one api version? I assume one api version supports multiple validations because breaking changes can be introduced with multiple api versions.
Also are there any mechanical way to prevent (or at least notice) breaking changes including validations?
Originally we wanted to avoid type assertion but that should be enough.
The IO costs for the rest of the request are much higher than the check in terms of performance, if that was the concern.
I found the mobile app example interesting. How do you guys keep multiple validations for one api version? I assume one api version supports multiple validations because breaking changes can be introduced with multiple api versions.
It's handled on a case by case basis, but we do allow the evolution of APIs assuming they don't break older clients. For significant changes, a new endpoint is certainly preferred. This managed through...
Also are there any mechanical way to prevent (or at least notice) breaking changes including validations?
Protolock! While it doesn't look at annotations, it does support plugins, so one could be written to be aware of PGV. We haven't gone that far yet, however.
The IO costs for the rest of the request are much higher than the check in terms of performance, if that was the concern.
True, IO should be orders of magnitude slower 🐌
In this case, at Lyft, we recommend clients perform validation with the version of the rules they know, while servers run the validations, but do not block "invalid" requests in production. In development/staging, it should be a hard InvalidArgument error though.
It's handled on a case by case basis, but we do allow the evolution of APIs assuming they don't break older clients. For significant changes, a new endpoint is certainly preferred. This managed through...
Could you elaborate? I understood that server runs a set of validations based on client version not to break old clients. I'm specially interested in keeping validations backward compatible because protolock ensures field level compatibility.
Protolock! While it doesn't look at annotations, it does support plugins, so one could be written to be aware of PGV. We haven't gone that far yet, however.
Thank you, I didn't know it supports plugins. I'll take a look.
ehm... https://github.com/grpc-ecosystem/go-grpc-middleware/blob/master/validator/validator.go