-
Notifications
You must be signed in to change notification settings - Fork 826
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
gRPC request validation (server-side) #487
Draft
anjeyy
wants to merge
1
commit into
grpc-ecosystem:master
Choose a base branch
from
anjeyy:grpc-validation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 42 additions & 0 deletions
42
...boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraint.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
/** | ||
* Marker annotation to scan for validation classes, which have to implement {@link GrpcConstraintValidator}. Scanning | ||
* is done in {@link GrpcValidationResolver}. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see GrpcConstraintValidator | ||
* @see GrpcValidationResolver | ||
*/ | ||
@Target(ElementType.TYPE) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Documented | ||
@Component | ||
public @interface GrpcConstraint { | ||
|
||
} |
49 changes: 49 additions & 0 deletions
49
...configure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintIsPresent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import java.util.Objects; | ||
|
||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; | ||
import org.springframework.context.annotation.ConditionContext; | ||
import org.springframework.context.annotation.ConfigurationCondition; | ||
import org.springframework.core.type.AnnotatedTypeMetadata; | ||
|
||
/** | ||
* Condition checking if annotation {@link GrpcConstraint @GrpcConstraint} is present. Used to indicate that classes can | ||
* be picked up for validation purpose with {@link GrpcValidationResolver}. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see GrpcConstraint | ||
* @see GrpcValidationResolver | ||
*/ | ||
class GrpcConstraintIsPresent implements ConfigurationCondition { | ||
|
||
@Override | ||
public ConfigurationPhase getConfigurationPhase() { | ||
return ConfigurationPhase.REGISTER_BEAN; | ||
} | ||
|
||
@Override | ||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { | ||
|
||
ConfigurableListableBeanFactory safeBeanFactory = | ||
Objects.requireNonNull(context.getBeanFactory(), "ConfigurableListableBeanFactory is null"); | ||
return !safeBeanFactory.getBeansWithAnnotation(GrpcConstraint.class).isEmpty(); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
...configure/src/main/java/net/devh/boot/grpc/server/validation/GrpcConstraintValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import com.google.protobuf.MessageLiteOrBuilder; | ||
|
||
import net.devh.boot.grpc.server.service.GrpcService; | ||
|
||
/** | ||
* Implement this interface to perform a request validation for incoming gRPC messages. Subsequently requests received | ||
* in {@link GrpcService @GrpcService} are validated.<br> | ||
* <b>Hint: </b> Also annotate class with {@link GrpcConstraint @GrpcConstraint} to be picked up. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see GrpcValidationResolver | ||
* @see GrpcConstraint | ||
*/ | ||
public interface GrpcConstraintValidator<E extends MessageLiteOrBuilder> { | ||
|
||
/** | ||
* Method invoked to check wheter validation succeds. In case an exeception occurs a | ||
* {@link io.grpc.Status.Code#INTERNAL} is sent back to the client with the thrown exception message. | ||
* | ||
* @param request gRPC request | ||
* @return {@code true} if validation successfull, {@code false otherwise} | ||
*/ | ||
boolean isValid(E request); | ||
|
||
} |
52 changes: 52 additions & 0 deletions
52
...utoconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Conditional; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.core.annotation.Order; | ||
|
||
import net.devh.boot.grpc.common.util.InterceptorOrder; | ||
import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; | ||
|
||
/** | ||
* In Order to have valid requests this autoconfiguration is looking for marker annotation | ||
* {@link GrpcConstraint @GrpcConstraint}. In case of success, all necessary beans are being instantiated. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see GrpcConstraint | ||
* @see GrpcValidationResolver | ||
* @see RequestValidationInterceptor | ||
*/ | ||
@Configuration | ||
@Conditional(GrpcConstraintIsPresent.class) | ||
class GrpcValidationConfig { | ||
|
||
@Bean | ||
GrpcValidationResolver grpcValidationResolver() { | ||
return new GrpcValidationResolver(); | ||
} | ||
|
||
@GrpcGlobalServerInterceptor | ||
@Order(InterceptorOrder.ORDER_SERVER_REQUEST_VALIDATION) | ||
RequestValidationInterceptor requestValidationInterceptor(final GrpcValidationResolver grpcValidationResolver) { | ||
return new RequestValidationInterceptor(grpcValidationResolver); | ||
} | ||
|
||
} |
111 changes: 111 additions & 0 deletions
111
...oconfigure/src/main/java/net/devh/boot/grpc/server/validation/GrpcValidationResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import java.lang.reflect.ParameterizedType; | ||
import java.lang.reflect.Type; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Map.Entry; | ||
import java.util.stream.Collectors; | ||
|
||
import org.springframework.beans.factory.InitializingBean; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.context.ApplicationContextAware; | ||
|
||
import com.google.protobuf.MessageLiteOrBuilder; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
|
||
/** | ||
* Resolving all classes implementing {@link GrpcConstraintValidator} and marked with annotation | ||
* {@link GrpcConstraint @GrpcConstraint}. Resolved classes are validation classes for gRPC requests to be validated. | ||
* <p> | ||
* The Validation is done via {@link RequestValidationInterceptor}. There can be more than one validation class for the | ||
* same request type, all of them are being resolved and used for validation. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see GrpcConstraintValidator | ||
* @see RequestValidationInterceptor | ||
*/ | ||
@Slf4j | ||
class GrpcValidationResolver implements InitializingBean, ApplicationContextAware { | ||
|
||
private Map<String, GrpcConstraintValidator<MessageLiteOrBuilder>> validatorMap; | ||
private ApplicationContext applicationContext; | ||
|
||
@Override | ||
public void setApplicationContext(ApplicationContext applicationContext) { | ||
this.applicationContext = applicationContext; | ||
} | ||
|
||
@Override | ||
public void afterPropertiesSet() throws Exception { | ||
|
||
validatorMap = applicationContext.getBeansWithAnnotation(GrpcConstraint.class) | ||
.entrySet() | ||
.stream() | ||
.collect(Collectors.toMap(Entry::getKey, this::convertSafely)); | ||
log.debug("Found {} gRPC validators", validatorMap.size()); | ||
} | ||
|
||
|
||
private GrpcConstraintValidator<MessageLiteOrBuilder> convertSafely(Map.Entry<String, Object> entry) { | ||
|
||
Object annotatedValidator = entry.getValue(); | ||
if (annotatedValidator instanceof GrpcConstraintValidator) { | ||
@SuppressWarnings("unchecked") | ||
GrpcConstraintValidator<MessageLiteOrBuilder> safeConstraintInstance = | ||
(GrpcConstraintValidator<MessageLiteOrBuilder>) annotatedValidator; | ||
return safeConstraintInstance; | ||
} | ||
|
||
throw new IllegalStateException( | ||
String.format("@GrpcConstraint annotated class [%s] has to implement GrpcConstraintValidator.class", | ||
annotatedValidator.getClass())); | ||
} | ||
|
||
/** | ||
* Retrieve all {@link GrpcConstraintValidator} which are the same class or at least a superclass of given input | ||
* parameter. | ||
* | ||
* @param request gRPC request | ||
* @param <E> type of the gRPC request message | ||
* @return validators to be used in conjunction with the request | ||
*/ | ||
<E> List<GrpcConstraintValidator<MessageLiteOrBuilder>> findValidators(E request) { | ||
return validatorMap.values() | ||
.stream() | ||
.filter(cs -> checkForGenericTypeArgument(cs, request)) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private <E> boolean checkForGenericTypeArgument( | ||
GrpcConstraintValidator<MessageLiteOrBuilder> grpcConstraintValidator, E request) { | ||
|
||
List<Type> genericTypes = Arrays.asList(grpcConstraintValidator.getClass().getGenericInterfaces()); | ||
|
||
return genericTypes.stream() | ||
.map(t -> (ParameterizedType) t) | ||
.flatMap(pt -> Arrays.stream(pt.getActualTypeArguments())) | ||
.map(t -> (Class<?>) t) | ||
.anyMatch(c -> c.isAssignableFrom(request.getClass())); | ||
} | ||
|
||
} |
52 changes: 52 additions & 0 deletions
52
...gure/src/main/java/net/devh/boot/grpc/server/validation/RequestValidationInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* Copyright (c) 2016-2021 Michael Zhang <[email protected]> | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the | ||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to | ||
* permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the | ||
* Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
|
||
package net.devh.boot.grpc.server.validation; | ||
|
||
import io.grpc.Metadata; | ||
import io.grpc.ServerCall; | ||
import io.grpc.ServerCall.Listener; | ||
import io.grpc.ServerCallHandler; | ||
import io.grpc.ServerInterceptor; | ||
|
||
/** | ||
* Interceptor to validate incoming gRPC requests. Validations are obtained from {@link GrpcValidationResolver} and | ||
* processed with {@link RequestValidationListener}. | ||
* | ||
* @author Andjelko Perisic ([email protected]) | ||
* @see RequestValidationListener | ||
* @see GrpcValidationResolver | ||
*/ | ||
class RequestValidationInterceptor implements ServerInterceptor { | ||
|
||
private final GrpcValidationResolver grpcValidationResolver; | ||
|
||
RequestValidationInterceptor(final GrpcValidationResolver grpcValidationResolver) { | ||
this.grpcValidationResolver = grpcValidationResolver; | ||
} | ||
|
||
@Override | ||
public <ReqT, RespT> Listener<ReqT> interceptCall( | ||
ServerCall<ReqT, RespT> call, | ||
Metadata headers, | ||
ServerCallHandler<ReqT, RespT> next) { | ||
|
||
Listener<ReqT> delegate = next.startCall(call, headers); | ||
return new RequestValidationListener<>(delegate, call, headers, grpcValidationResolver); | ||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if what i know is right, reason printing call already closed is
next.startCall
is non thread-safe.solution : https://groups.google.com/g/grpc-io/c/_osH2D6L9Ck
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what exactly happens, but it's probably not related to mutli-threading, but "owning" the server call instance.
You only close the call, but you never tell the control structure, that you have done so, thus the request processing "continues" as normal. Then the close you have send kicks in and the server tries to properly close the connection, thus sending the close twice. Which is probably the exception you are seeing.
Instead of (or maybe in addition to) closing the call instance, you have to throw a
RuntimeStatusException
. This will cause the control structure to shutdown with you error status and thus avoids the duplicate close.(I haven't tested this, just my guess from your one sentence without stacktrace)
I did something similar here:
https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java#L44
Alternatively do it like this (This is also from a validation framework, so it might work better for your usecase):
https://github.com/envoyproxy/protoc-gen-validate/blob/5ef93ae28a92362ede78aaa49c5c3e290c70e324/java/pgv-java-grpc/src/main/java/io/envoyproxy/pgv/grpc/ValidatingServerInterceptor.java
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh.. thank you 😄