fix: handle null or blank rpcExt(#6650) - #7146
Conversation
Fall back to default gRPC extension settings when rpcExt deserialization returns null. Use a 5000ms timeout and UNARY method type, and add regression coverage for null, blank, and explicitly configured rpcExt values.
utafrali
left a comment
There was a problem hiding this comment.
The null-rpcExt fix and the default UNARY method type are correct improvements, but the blank-string handling relies on Gson returning null for whitespace input rather than throwing, which is not guaranteed across Gson versions and will silently regress on a Gson upgrade to 2.9+.
| .orElse(StringUtils.EMPTY)).attach(); | ||
|
|
||
| GrpcExtInfo extInfo = GsonUtils.getGson().fromJson(metaData.getRpcExt(), GrpcExtInfo.class); | ||
| GrpcExtInfo extInfo = Optional.ofNullable(GsonUtils.getGson().fromJson(metaData.getRpcExt(), GrpcExtInfo.class)) |
There was a problem hiding this comment.
The Optional.ofNullable wrapper handles the case where Gson returns null (which covers a null input), but it does not protect against JsonSyntaxException, which Gson throws for blank/whitespace-only strings in Gson 2.9+. On those versions, fromJson(" ", GrpcExtInfo.class) throws rather than returns null, so this fix silently breaks the blank-string case on a Gson upgrade.
A more robust approach is to guard with a blank check before calling fromJson:
String rpcExt = metaData.getRpcExt();
GrpcExtInfo extInfo = StringUtils.isBlank(rpcExt)
? new GrpcExtInfo()
: Optional.ofNullable(GsonUtils.getGson().fromJson(rpcExt, GrpcExtInfo.class))
.orElseGet(GrpcExtInfo::new);This is explicit about intent and does not rely on Gson's version-specific behavior for whitespace input.
| throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { | ||
| ServerWebExchange exchange = getServerWebExchange(); | ||
| MetaData metaData = getMetaData(); | ||
| metaData.setRpcExt(" "); |
There was a problem hiding this comment.
The blank test only covers a single-space string " ". An empty string "" is a distinct boundary case — some Gson versions return null for it while others throw JsonSyntaxException. Adding a testDoExecuteWithEmptyRpcExt alongside this one (with metaData.setRpcExt("")) would make it clear that both are handled and would catch a regression if Gson behavior changes.
There was a problem hiding this comment.
Thanks for your review. I have added a separate test for an empty rpcExt value and moved the blank check before Gson deserialization. Null, empty, and whitespace-only values are now handled explicitly without relying on Gson's behavior.
Handle null, empty, and whitespace-only rpcExt values before Gson deserialization to avoid version-dependent parsing behavior. Fall back to default gRPC settings and add regression tests for each boundary case.
| .orElse(StringUtils.EMPTY)).attach(); | ||
|
|
||
| GrpcExtInfo extInfo = GsonUtils.getGson().fromJson(metaData.getRpcExt(), GrpcExtInfo.class); | ||
| GrpcExtInfo extInfo = StringUtils.isBlank(metaData.getRpcExt()) |
There was a problem hiding this comment.
Suggestion (non-blocking): the guard covers the case where Gson returns a null object, but not the case where individual fields end up null. If rpcExt contains "methodType": null, or an enum name Gson cannot resolve, the field is left null and ShenyuGrpcClient#invoke still NPEs on switch (methodType) (same for JsonMessage#getMethodType).
A small normalisation after parsing would close the same class of bug completely - e.g. after building extInfo, if (Objects.isNull(extInfo.getMethodType())) { extInfo.setMethodType(MethodDescriptor.MethodType.UNARY); } with a warn log.
Aias00
left a comment
There was a problem hiding this comment.
Approving - this is a real crash fix, not just defensive plumbing.
I traced the call chain to confirm the impact: GrpcPlugin#doExecute passes extInfo.methodType into ShenyuGrpcClient#call(...), whose invoke(...) does switch (methodType), and JsonMessage#getMethodType(...) switches on it too. Both throw NullPointerException for a null methodType. Gson's fromJson(String, Class) returns null for both a null and an empty rpcExt, so before this change any grpc metadata registered without a rpcExt (or with "methodType" missing) blew up inside the plugin chain - exactly #6650.
So the combination here is right: skip the parse when rpcExt is blank, fall back to a default instance when Gson still returns null (literal null), and give methodType a non-null default instead of relying on the missing key. The three added tests (null / "" / " ") cover precisely what was previously untested, and switching the fixture to strict JSON {"timeout":5000,"methodType":"SERVER_STREAMING"} with an assertion on the parsed method type makes executeRequest actually assert something now.
Two follow-ups, neither blocking:
-
The guard handles a null object, not null fields. If
rpcExtparses but leavesmethodTypenull - an unresolvable enum name, or an explicit"methodType": null- the field ends up null andinvoke(...)still NPEs. A normalisation right after parsing would close that residual path completely (see inline comment). -
Defaulting to
UNARYbeats crashing, but it now silently assumes unary semantics for metadata that simply forgot to declare the method type, so a mis-registered streaming method produces a confusing downstream result instead of an immediate NPE. Consider aLOG.warnwhen the defaults are used. Related nit: a{"timeout":0}payload yields a 0 ms deadline; keeping the 5000 default when the parsed value is null or <= 0 would be safer.
CI is fully green (build on 17 and 21, all integrated tests, all e2e cases including the storage ones), so nothing blocks merge.
Fixes #6650
Summary
rpcExtvalues inGrpcPlugin.GrpcExtInfowhen Gson deserialization returns null.Test
rpcExt.rpcExt.UNARYmethod type without throwing an NPE.SERVER_STREAMINGmethod type is preserved.Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.