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

grpc / grpc-java / #20341

01 Jul 2026 06:00AM UTC coverage: 89.007% (-0.03%) from 89.033%
#20341

push

github

web-flow
xds: Refactor to allow common code usage between the ext_proc client and server interceptors (#12883)

Moved some of the util classes for ext_proc to a new sub package io.grpc.xds.internal.extproc.

37578 of 42219 relevant lines covered (89.01%)

0.89 hits per line

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

96.53
/../xds/src/main/java/io/grpc/xds/ExternalProcessorFilter.java
1
/*
2
 * Copyright 2024 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;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import com.google.common.collect.ImmutableList;
23
import com.google.protobuf.Any;
24
import com.google.protobuf.Duration;
25
import com.google.protobuf.InvalidProtocolBufferException;
26
import com.google.protobuf.Message;
27
import com.google.protobuf.util.Durations;
28
import io.envoyproxy.envoy.config.core.v3.GrpcService;
29
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ExtProcOverrides;
30
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ExtProcPerRoute;
31
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor;
32
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ProcessingMode;
33
import io.grpc.ClientInterceptor;
34
import io.grpc.internal.GrpcUtil;
35
import io.grpc.xds.internal.HeaderForwardingRulesConfig;
36
import io.grpc.xds.internal.grpcservice.CachedChannelManager;
37
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig;
38
import io.grpc.xds.internal.grpcservice.GrpcServiceParseException;
39
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
40
import io.grpc.xds.internal.headermutations.HeaderMutationRulesParseException;
41
import io.grpc.xds.internal.headermutations.HeaderMutationRulesParser;
42
import java.util.List;
43
import java.util.Optional;
44
import java.util.concurrent.ScheduledExecutorService;
45
import java.util.concurrent.TimeUnit;
46
import javax.annotation.Nullable;
47

48
/**
49
 * Filter for external processing as per gRFC A93.
50
 */
51
public class ExternalProcessorFilter implements Filter {
52
  static final String TYPE_URL = 
53
      "type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor";
54

55
  private final CachedChannelManager cachedChannelManager;
56
  private final FilterContext context;
57

58
  public ExternalProcessorFilter(FilterContext context) {
59
    this(context, new CachedChannelManager());
1✔
60
  }
1✔
61

62
  @VisibleForTesting
63
  ExternalProcessorFilter(FilterContext context, CachedChannelManager cachedChannelManager) {
1✔
64
    this.context = checkNotNull(context, "context");
1✔
65
    this.cachedChannelManager = checkNotNull(cachedChannelManager, "cachedChannelManager");
1✔
66
  }
1✔
67

68
  @Override
69
  public void close() {
70
    cachedChannelManager.close();
1✔
71
  }
1✔
72

73
  static final class Provider implements Filter.Provider {
1✔
74
    @Override
75
    public String[] typeUrls() {
76
      return new String[]{TYPE_URL};
1✔
77
    }
78

79
    @Override
80
    public boolean isClientFilter() {
81
      return GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false);
1✔
82
    }
83

84
    @Override
85
    public ExternalProcessorFilter newInstance(FilterContext context) {
86
      return new ExternalProcessorFilter(context);
×
87
    }
88

89
    @Override
90
    public ConfigOrError<ExternalProcessorFilterConfig> parseFilterConfig(
91
        Message rawProtoMessage, FilterConfigParseContext context) {
92
      if (!(rawProtoMessage instanceof Any)) {
1✔
93
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
×
94
      }
95
      ExternalProcessor externalProcessor;
96
      try {
97
        externalProcessor = ((Any) rawProtoMessage).unpack(ExternalProcessor.class);
1✔
98
      } catch (InvalidProtocolBufferException e) {
1✔
99
        return ConfigOrError.fromError("Invalid proto: " + e);
1✔
100
      }
1✔
101

102
      return ExternalProcessorFilterConfig.create(externalProcessor, context);
1✔
103
    }
104

105
    @Override
106
    public ConfigOrError<ExternalProcessorFilterOverrideConfig> parseFilterConfigOverride(
107
        Message rawProtoMessage, FilterConfigParseContext context) {
108
      if (!(rawProtoMessage instanceof Any)) {
1✔
109
        return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
×
110
      }
111
      ExtProcPerRoute perRoute;
112
      try {
113
        perRoute = ((Any) rawProtoMessage).unpack(ExtProcPerRoute.class);
1✔
114
      } catch (InvalidProtocolBufferException e) {
1✔
115
        return ConfigOrError.fromError("Invalid proto: " + e);
1✔
116
      }
1✔
117
      ExtProcOverrides overrides = perRoute.hasOverrides()
1✔
118
          ? perRoute.getOverrides() : ExtProcOverrides.getDefaultInstance();
1✔
119
      return ExternalProcessorFilterOverrideConfig.create(overrides, context);
1✔
120
    }
121
  }
122

123
  @Nullable
124
  @Override
125
  public ClientInterceptor buildClientInterceptor(FilterConfig filterConfig,
126
      @Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
127
    ExternalProcessorFilterConfig extProcFilterConfig =
1✔
128
        (ExternalProcessorFilterConfig) filterConfig;
129
    if (overrideConfig != null) {
1✔
130
      extProcFilterConfig = mergeConfigs(extProcFilterConfig,
1✔
131
          (ExternalProcessorFilterOverrideConfig) overrideConfig);
132
    }
133
    return new ExternalProcessorClientInterceptor(
1✔
134
        extProcFilterConfig, cachedChannelManager, scheduler, context);
135
  }
136

137
  private static ExternalProcessorFilterConfig mergeConfigs(
138
      ExternalProcessorFilterConfig extProcFilterConfig,
139
      ExternalProcessorFilterOverrideConfig extProcFilterConfigOverride) {
140
    ExternalProcessor parentProto = extProcFilterConfig.getExternalProcessor();
1✔
141
    ExternalProcessor.Builder mergedProtoBuilder = parentProto.toBuilder();
1✔
142

143
    if (extProcFilterConfigOverride.hasProcessingMode()) {
1✔
144
      mergedProtoBuilder.setProcessingMode(extProcFilterConfigOverride.getProcessingMode());
1✔
145
    }
146

147
    if (extProcFilterConfigOverride.hasRequestAttributes()) {
1✔
148
      mergedProtoBuilder.clearRequestAttributes()
1✔
149
          .addAllRequestAttributes(extProcFilterConfigOverride.getRequestAttributesList());
1✔
150
    }
151
    if (extProcFilterConfigOverride.hasResponseAttributes()) {
1✔
152
      mergedProtoBuilder.clearResponseAttributes()
1✔
153
          .addAllResponseAttributes(extProcFilterConfigOverride.getResponseAttributesList());
1✔
154
    }
155
    if (extProcFilterConfigOverride.hasGrpcService()) {
1✔
156
      mergedProtoBuilder.setGrpcService(extProcFilterConfigOverride.getGrpcService());
1✔
157
    }
158

159
    if (extProcFilterConfigOverride.hasFailureModeAllow()) {
1✔
160
      mergedProtoBuilder.setFailureModeAllow(extProcFilterConfigOverride.getFailureModeAllow());
1✔
161
    }
162

163
    GrpcServiceConfig grpcServiceConfig =
164
        extProcFilterConfigOverride.getGrpcServiceConfig() != null
1✔
165
            ? extProcFilterConfigOverride.getGrpcServiceConfig()
1✔
166
            : extProcFilterConfig.getGrpcServiceConfig();
1✔
167

168
    return new ExternalProcessorFilterConfig(
1✔
169
        mergedProtoBuilder.build(),
1✔
170
        grpcServiceConfig,
171
        extProcFilterConfig.getMutationRulesConfig(),
1✔
172
        extProcFilterConfig.getForwardRulesConfig());
1✔
173
  }
174

175
  private static ConfigOrError<Optional<GrpcServiceConfig>> parseAndValidate(
176
      ProcessingMode mode,
177
      GrpcService grpcService,
178
      boolean isParent,
179
      FilterConfigParseContext context) {
180
    if (mode.getRequestBodyMode() != ProcessingMode.BodySendMode.GRPC
1✔
181
        && mode.getRequestBodyMode() != ProcessingMode.BodySendMode.NONE) {
1✔
182
      return ConfigOrError.fromError("Invalid request_body_mode: " + mode.getRequestBodyMode()
1✔
183
          + ". Only GRPC and NONE are supported.");
184
    }
185
    if (mode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC
1✔
186
        && mode.getResponseBodyMode() != ProcessingMode.BodySendMode.NONE) {
1✔
187
      return ConfigOrError.fromError("Invalid response_body_mode: " + mode.getResponseBodyMode()
1✔
188
          + ". Only GRPC and NONE are supported.");
189
    }
190

191
    if (mode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC
1✔
192
        && mode.getResponseTrailerMode() != ProcessingMode.HeaderSendMode.SEND) {
1✔
193
      return ConfigOrError.fromError(
1✔
194
          "Invalid response_trailer_mode: " + mode.getResponseTrailerMode()
1✔
195
              + ". response_trailer_mode must be SEND if response_body_mode is GRPC.");
196
    }
197

198
    try {
199
      if (grpcService != null && grpcService.hasGoogleGrpc()) {
1✔
200
        GrpcServiceConfig grpcServiceConfig = GrpcServiceConfigParser.parse(
1✔
201
            grpcService, context.bootstrapInfo(), context.serverInfo());
1✔
202
        return ConfigOrError.fromConfig(Optional.of(grpcServiceConfig));
1✔
203
      } else if (isParent) {
1✔
204
        return ConfigOrError.fromError("Error parsing GrpcService config: " 
1✔
205
            + "Unsupported: GrpcService must have GoogleGrpc, got: " + grpcService);
206
      }
207
      return ConfigOrError.fromConfig(Optional.empty());
1✔
208
    } catch (GrpcServiceParseException e) {
1✔
209
      return ConfigOrError.fromError("Error parsing GrpcService config: " + e.getMessage());
1✔
210
    }
211
  }
212

213
  static final class ExternalProcessorFilterConfig implements FilterConfig {
214

215
    private final ExternalProcessor externalProcessor;
216
    private final GrpcServiceConfig grpcServiceConfig;
217
    private final Optional<HeaderMutationRulesConfig> mutationRulesConfig;
218
    private final Optional<HeaderForwardingRulesConfig> forwardRulesConfig;
219

220
    static ConfigOrError<ExternalProcessorFilterConfig> create(
221
        ExternalProcessor externalProcessor, FilterConfigParseContext context) {
222
      ProcessingMode mode = externalProcessor.getProcessingMode();
1✔
223
      GrpcService grpcService = externalProcessor.getGrpcService();
1✔
224
      HeaderMutationRulesConfig mutationRulesConfig = null;
1✔
225
      HeaderForwardingRulesConfig forwardRulesConfig = null;
1✔
226

227
      if (externalProcessor.hasMutationRules()) {
1✔
228
        try {
229
          mutationRulesConfig = 
1✔
230
              HeaderMutationRulesParser.parse(externalProcessor.getMutationRules());
1✔
231
        } catch (HeaderMutationRulesParseException e) {
1✔
232
          return ConfigOrError.fromError("Error parsing HeaderMutationRules: " + e.getMessage());
1✔
233
        }
1✔
234
      }
235

236
      if (externalProcessor.hasForwardRules()) {
1✔
237
        forwardRulesConfig = 
1✔
238
            HeaderForwardingRulesConfig.create(externalProcessor.getForwardRules());
1✔
239
      }
240

241
      if (externalProcessor.hasDeferredCloseTimeout()) {
1✔
242
        Duration deferredCloseTimeout = externalProcessor.getDeferredCloseTimeout();
1✔
243
        try {
244
          Durations.checkValid(deferredCloseTimeout);
1✔
245
        } catch (IllegalArgumentException e) {
1✔
246
          return ConfigOrError.fromError("Invalid deferred_close_timeout: " + e.getMessage());
1✔
247
        }
1✔
248
        long deferredCloseTimeoutNanos = Durations.toNanos(deferredCloseTimeout);
1✔
249
        if (deferredCloseTimeoutNanos <= 0) {
1✔
250
          return ConfigOrError.fromError("deferred_close_timeout must be positive");
1✔
251
        }
252
      }
253

254
      ConfigOrError<Optional<GrpcServiceConfig>> parsed =
1✔
255
          parseAndValidate(mode, grpcService, true, context);
1✔
256
      if (parsed.errorDetail != null) {
1✔
257
        return ConfigOrError.fromError(parsed.errorDetail);
1✔
258
      }
259

260
      return ConfigOrError.fromConfig(new ExternalProcessorFilterConfig(
1✔
261
          externalProcessor, parsed.config.orElse(null), 
1✔
262
          Optional.ofNullable(mutationRulesConfig), 
1✔
263
          Optional.ofNullable(forwardRulesConfig)));
1✔
264
    }
265

266
    ExternalProcessorFilterConfig(
267
        ExternalProcessor externalProcessor,
268
        GrpcServiceConfig grpcServiceConfig,
269
        Optional<HeaderMutationRulesConfig> mutationRulesConfig,
270
        Optional<HeaderForwardingRulesConfig> forwardRulesConfig) {
1✔
271
      this.externalProcessor = checkNotNull(externalProcessor, "externalProcessor");
1✔
272
      this.grpcServiceConfig = grpcServiceConfig;
1✔
273
      this.mutationRulesConfig = mutationRulesConfig;
1✔
274
      this.forwardRulesConfig = forwardRulesConfig;
1✔
275
    }
1✔
276

277
    @Override
278
    public String typeUrl() {
279
      return TYPE_URL;
1✔
280
    }
281

282
    ExternalProcessor getExternalProcessor() {
283
      return externalProcessor;
1✔
284
    }
285

286
    GrpcServiceConfig getGrpcServiceConfig() {
287
      return grpcServiceConfig;
1✔
288
    }
289

290
    Optional<HeaderMutationRulesConfig> getMutationRulesConfig() {
291
      return mutationRulesConfig;
1✔
292
    }
293

294
    Optional<HeaderForwardingRulesConfig> getForwardRulesConfig() {
295
      return forwardRulesConfig;
1✔
296
    }
297

298
    ImmutableList<String> getRequestAttributes() {
299
      return ImmutableList.copyOf(externalProcessor.getRequestAttributesList());
1✔
300
    }
301

302
    boolean getDisableImmediateResponse() {
303
      return externalProcessor.getDisableImmediateResponse();
1✔
304
    }
305

306
    long getDeferredCloseTimeoutNanos() {
307
      if (externalProcessor.hasDeferredCloseTimeout()) {
1✔
308
        return Durations.toNanos(externalProcessor.getDeferredCloseTimeout());
1✔
309
      }
310
      return TimeUnit.SECONDS.toNanos(5);
×
311
    }
312

313
    boolean getObservabilityMode() {
314
      return externalProcessor.getObservabilityMode();
1✔
315
    }
316

317
    boolean getFailureModeAllow() {
318
      return externalProcessor.getFailureModeAllow();
1✔
319
    }
320
  }
321

322
  static final class ExternalProcessorFilterOverrideConfig implements FilterConfig {
323
    private final ExtProcOverrides extProcOverrides;
324
    private final GrpcServiceConfig grpcServiceConfig;
325

326
    static ConfigOrError<ExternalProcessorFilterOverrideConfig> create(
327
        ExtProcOverrides overrides, FilterConfigParseContext context) {
328
      ConfigOrError<Optional<GrpcServiceConfig>> parsed =
1✔
329
          parseAndValidate(
1✔
330
              overrides.getProcessingMode(), overrides.getGrpcService(), false, context);
1✔
331
      if (parsed.errorDetail != null) {
1✔
332
        return ConfigOrError.fromError(parsed.errorDetail);
1✔
333
      }
334
      return ConfigOrError.fromConfig(
1✔
335
          new ExternalProcessorFilterOverrideConfig(overrides, parsed.config.orElse(null)));
1✔
336
    }
337

338
    ExternalProcessorFilterOverrideConfig(
339
        ExtProcOverrides extProcOverrides, GrpcServiceConfig grpcServiceConfig) {
1✔
340
      this.extProcOverrides = checkNotNull(extProcOverrides, "extProcOverrides");
1✔
341
      this.grpcServiceConfig = grpcServiceConfig;
1✔
342
    }
1✔
343

344
    @Override
345
    public String typeUrl() {
346
      return TYPE_URL;
×
347
    }
348

349
    boolean hasProcessingMode() {
350
      return extProcOverrides.hasProcessingMode();
1✔
351
    }
352

353
    ProcessingMode getProcessingMode() {
354
      return extProcOverrides.getProcessingMode();
1✔
355
    }
356

357
    boolean hasRequestAttributes() {
358
      return extProcOverrides.getRequestAttributesCount() > 0;
1✔
359
    }
360

361
    List<String> getRequestAttributesList() {
362
      return extProcOverrides.getRequestAttributesList();
1✔
363
    }
364

365
    boolean hasResponseAttributes() {
366
      return extProcOverrides.getResponseAttributesCount() > 0;
1✔
367
    }
368

369
    List<String> getResponseAttributesList() {
370
      return extProcOverrides.getResponseAttributesList();
1✔
371
    }
372

373
    boolean hasGrpcService() {
374
      return extProcOverrides.hasGrpcService();
1✔
375
    }
376

377
    GrpcService getGrpcService() {
378
      return extProcOverrides.getGrpcService();
1✔
379
    }
380

381
    boolean hasFailureModeAllow() {
382
      return extProcOverrides.hasFailureModeAllow();
1✔
383
    }
384

385
    boolean getFailureModeAllow() {
386
      return extProcOverrides.hasFailureModeAllow()
1✔
387
          && extProcOverrides.getFailureModeAllow().getValue();
1✔
388
    }
389

390
    GrpcServiceConfig getGrpcServiceConfig() {
391
      return grpcServiceConfig;
1✔
392
    }
393
  }
394

395

396
}
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