• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20141

07 Jan 2026 03:51PM UTC coverage: 88.665% (+0.006%) from 88.659%
#20141

push

github

ejona86
Catch Errors when calling complex parsing code

When we have involved parsing/validation logic, it would be easy to have
bugs. There's little reason we can't handle those bugs, as the logic
shouldn't be modifying global state. While the current config is bad or
trigger a bug, we want to keep working until we get good/working config
again.

For XDS, we do have the problem of losing the exception's stack trace.
We could consider increasing the log level, but we could also consider
propagating the error to the listener. Let's skip figuring out that
exact behavior for now, and just move a step in the right direction.

35326 of 39842 relevant lines covered (88.67%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.47
/../xds/src/main/java/io/grpc/xds/client/XdsResourceType.java
1
/*
2
 * Copyright 2022 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.xds.client;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.xds.client.XdsClient.canonifyResourceName;
21
import static io.grpc.xds.client.XdsClient.isResourceNameValid;
22

23
import com.google.protobuf.Any;
24
import com.google.protobuf.InvalidProtocolBufferException;
25
import com.google.protobuf.Message;
26
import io.envoyproxy.envoy.service.discovery.v3.Resource;
27
import io.grpc.ExperimentalApi;
28
import io.grpc.xds.client.Bootstrapper.ServerInfo;
29
import io.grpc.xds.client.XdsClient.ResourceUpdate;
30
import java.util.ArrayList;
31
import java.util.HashMap;
32
import java.util.HashSet;
33
import java.util.List;
34
import java.util.Map;
35
import java.util.Set;
36
import java.util.logging.Level;
37
import java.util.logging.Logger;
38
import javax.annotation.Nullable;
39

40
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10847")
41
public abstract class XdsResourceType<T extends ResourceUpdate> {
1✔
42
  private static final Logger log = Logger.getLogger(XdsResourceType.class.getName());
1✔
43

44
  static final String TYPE_URL_RESOURCE =
45
      "type.googleapis.com/envoy.service.discovery.v3.Resource";
46
  protected static final String TRANSPORT_SOCKET_NAME_TLS = "envoy.transport_sockets.tls";
47

48
  protected static final String TYPE_URL_TYPED_STRUCT_UDPA =
49
      "type.googleapis.com/udpa.type.v1.TypedStruct";
50
  protected static final String TYPE_URL_TYPED_STRUCT =
51
      "type.googleapis.com/xds.type.v3.TypedStruct";
52

53
  /**
54
   * Extract the resource name from an older resource type that included the name within the
55
   * resource contents itself. The newer approach has resources wrapped with {@code
56
   * envoy.service.discovery.v3.Resource} which then provides the name. This method is only called
57
   * for the old approach.
58
   *
59
   * @return the resource's name, or {@code null} if name is not stored within the resource contents
60
   */
61
  @Nullable
62
  protected String extractResourceName(Message unpackedResource) {
63
    return null;
×
64
  }
65

66
  protected abstract Class<? extends com.google.protobuf.Message> unpackedClassName();
67

68
  public abstract String typeName();
69

70
  public abstract String typeUrl();
71

72
  public abstract boolean shouldRetrieveResourceKeysForArgs();
73

74
  // Do not confuse with the SotW approach: it is the mechanism in which the client must specify all
75
  // resource names it is interested in with each request. Different resource types may behave
76
  // differently in this approach. For LDS and CDS resources, the server must return all resources
77
  // that the client has subscribed to in each request. For RDS and EDS, the server may only return
78
  // the resources that need an update.
79
  protected abstract boolean isFullStateOfTheWorld();
80

81
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10847")
82
  public static class Args {
83
    final ServerInfo serverInfo;
84
    final String versionInfo;
85
    final String nonce;
86
    final Bootstrapper.BootstrapInfo bootstrapInfo;
87
    final Object securityConfig;
88
    // Management server is required to always send newly requested resources, even if they
89
    // may have been sent previously (proactively). Thus, client does not need to cache
90
    // unrequested resources.
91
    // Only resources in the set needs to be parsed. Null means parse everything.
92
    final @Nullable Set<String> subscribedResources;
93

94
    public Args(ServerInfo serverInfo, String versionInfo, String nonce,
95
                Bootstrapper.BootstrapInfo bootstrapInfo,
96
                Object securityConfig,
97
                @Nullable Set<String> subscribedResources) {
1✔
98
      this.serverInfo = serverInfo;
1✔
99
      this.versionInfo = versionInfo;
1✔
100
      this.nonce = nonce;
1✔
101
      this.bootstrapInfo = bootstrapInfo;
1✔
102
      this.securityConfig = securityConfig;
1✔
103
      this.subscribedResources = subscribedResources;
1✔
104
    }
1✔
105

106
    public ServerInfo getServerInfo() {
107
      return serverInfo;
1✔
108
    }
109

110
    public String getNonce() {
111
      return nonce;
×
112
    }
113

114
    public Bootstrapper.BootstrapInfo getBootstrapInfo() {
115
      return bootstrapInfo;
1✔
116
    }
117

118
    public Object getSecurityConfig() {
119
      return securityConfig;
1✔
120
    }
121
  }
122

123
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10847")
124
  public static final class ResourceInvalidException extends Exception {
125
    private static final long serialVersionUID = 0L;
126

127
    public ResourceInvalidException(String message) {
128
      super(message, null, false, false);
1✔
129
    }
1✔
130

131
    public ResourceInvalidException(String message, Throwable cause) {
132
      super(cause != null ? message + ": " + cause.getMessage() : message, cause, false, false);
1✔
133
    }
1✔
134
  }
135

136
  ValidatedResourceUpdate<T> parse(Args args, List<Any> resources) {
137
    Map<String, ParsedResource<T>> parsedResources = new HashMap<>(resources.size());
1✔
138
    Set<String> unpackedResources = new HashSet<>(resources.size());
1✔
139
    Set<String> invalidResources = new HashSet<>();
1✔
140
    List<String> errors = new ArrayList<>();
1✔
141

142
    for (int i = 0; i < resources.size(); i++) {
1✔
143
      Any resource = resources.get(i);
1✔
144

145
      Message unpackedMessage;
146
      String name = "";
1✔
147
      try {
148
        if (resource.getTypeUrl().equals(TYPE_URL_RESOURCE)) {
1✔
149
          Resource wrappedResource = unpackCompatibleType(resource, Resource.class,
1✔
150
              TYPE_URL_RESOURCE, null);
151
          resource = wrappedResource.getResource();
1✔
152
          name = wrappedResource.getName();
1✔
153
        } 
154
        unpackedMessage = unpackCompatibleType(resource, unpackedClassName(), typeUrl(), null);
1✔
155
      } catch (InvalidProtocolBufferException e) {
1✔
156
        errors.add(String.format("%s response Resource index %d - can't decode %s: %s",
1✔
157
                typeName(), i, unpackedClassName().getSimpleName(), e.getMessage()));
1✔
158
        continue;
1✔
159
      }
1✔
160
      // Fallback to inner resource name if the outer resource didn't have a name.
161
      if (name.isEmpty()) {
1✔
162
        name = extractResourceName(unpackedMessage);
1✔
163
      }
164
      if (name == null || !isResourceNameValid(name, resource.getTypeUrl())) {
1✔
165
        errors.add(
1✔
166
            "Unsupported resource name: " + name + " for type: " + typeName());
1✔
167
        continue;
1✔
168
      }
169
      String cname = canonifyResourceName(name);
1✔
170
      if (args.subscribedResources != null && !args.subscribedResources.contains(name)) {
1✔
171
        continue;
1✔
172
      }
173
      unpackedResources.add(cname);
1✔
174

175
      T resourceUpdate;
176
      try {
177
        resourceUpdate = doParse(args, unpackedMessage);
1✔
178
      } catch (ResourceInvalidException e) {
1✔
179
        errors.add(String.format("%s response %s '%s' validation error: %s",
1✔
180
                typeName(), unpackedClassName().getSimpleName(), cname, e.getMessage()));
1✔
181
        invalidResources.add(cname);
1✔
182
        continue;
1✔
183
      } catch (Throwable t) {
1✔
184
        log.log(Level.FINE, "Unexpected error in doParse()", t);
1✔
185
        String errorMessage = t.getClass().getSimpleName();
1✔
186
        if (t.getMessage() != null) {
1✔
187
          errorMessage = errorMessage + ": " + t.getMessage();
1✔
188
        }
189
        errors.add(String.format("%s response '%s' unexpected error: %s",
1✔
190
                typeName(), cname, errorMessage));
1✔
191
        invalidResources.add(cname);
1✔
192
        continue;
1✔
193
      }
1✔
194

195
      // Resource parsed successfully.
196
      parsedResources.put(cname, new ParsedResource<T>(resourceUpdate, resource));
1✔
197
    }
198
    return new ValidatedResourceUpdate<T>(parsedResources, unpackedResources, invalidResources,
1✔
199
        errors);
200

201
  }
202

203
  protected abstract T doParse(Args args, Message unpackedMessage) throws ResourceInvalidException;
204

205
  /**
206
   * Helper method to unpack serialized {@link com.google.protobuf.Any} message, while replacing
207
   * Type URL {@code compatibleTypeUrl} with {@code typeUrl}.
208
   *
209
   * @param <T> The type of unpacked message
210
   * @param any serialized message to unpack
211
   * @param clazz the class to unpack the message to
212
   * @param typeUrl type URL to replace message Type URL, when it's compatible
213
   * @param compatibleTypeUrl compatible Type URL to be replaced with {@code typeUrl}
214
   * @return Unpacked message
215
   * @throws InvalidProtocolBufferException if the message couldn't be unpacked
216
   */
217
  protected static <T extends com.google.protobuf.Message> T unpackCompatibleType(
218
      Any any, Class<T> clazz, String typeUrl, String compatibleTypeUrl)
219
      throws InvalidProtocolBufferException {
220
    if (any.getTypeUrl().equals(compatibleTypeUrl)) {
1✔
221
      any = any.toBuilder().setTypeUrl(typeUrl).build();
1✔
222
    }
223
    return any.unpack(clazz);
1✔
224
  }
225

226
  static final class ParsedResource<T extends ResourceUpdate> {
227
    private final T resourceUpdate;
228
    private final Any rawResource;
229

230
    public ParsedResource(T resourceUpdate, Any rawResource) {
1✔
231
      this.resourceUpdate = checkNotNull(resourceUpdate, "resourceUpdate");
1✔
232
      this.rawResource = checkNotNull(rawResource, "rawResource");
1✔
233
    }
1✔
234

235
    T getResourceUpdate() {
236
      return resourceUpdate;
1✔
237
    }
238

239
    Any getRawResource() {
240
      return rawResource;
1✔
241
    }
242
  }
243

244
  static final class ValidatedResourceUpdate<T extends ResourceUpdate> {
245
    Map<String, ParsedResource<T>> parsedResources;
246
    Set<String> unpackedResources;
247
    Set<String> invalidResources;
248
    List<String> errors;
249

250
    // validated resource update
251
    public ValidatedResourceUpdate(Map<String, ParsedResource<T>> parsedResources,
252
                                   Set<String> unpackedResources,
253
                                   Set<String> invalidResources,
254
                                   List<String> errors) {
1✔
255
      this.parsedResources = parsedResources;
1✔
256
      this.unpackedResources = unpackedResources;
1✔
257
      this.invalidResources = invalidResources;
1✔
258
      this.errors = errors;
1✔
259
    }
1✔
260
  }
261
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc