• 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.28
/../xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.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
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.applyHeaderMutations;
21
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.collectAttributes;
22
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markDataPlaneCallClosed;
23
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markExtProcStreamCompleted;
24
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markExtProcStreamFailed;
25
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.outboundStreamToByteString;
26
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.toHeaderMap;
27

28
import com.google.common.annotations.VisibleForTesting;
29
import com.google.common.collect.ImmutableList;
30
import com.google.common.collect.ImmutableMap;
31
import com.google.protobuf.ByteString;
32
import com.google.protobuf.Struct;
33
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ProcessingMode;
34
import io.envoyproxy.envoy.service.ext_proc.v3.BodyMutation;
35
import io.envoyproxy.envoy.service.ext_proc.v3.BodyResponse;
36
import io.envoyproxy.envoy.service.ext_proc.v3.CommonResponse;
37
import io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc;
38
import io.envoyproxy.envoy.service.ext_proc.v3.HttpBody;
39
import io.envoyproxy.envoy.service.ext_proc.v3.HttpHeaders;
40
import io.envoyproxy.envoy.service.ext_proc.v3.HttpTrailers;
41
import io.envoyproxy.envoy.service.ext_proc.v3.ImmediateResponse;
42
import io.envoyproxy.envoy.service.ext_proc.v3.ProcessingRequest;
43
import io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse;
44
import io.envoyproxy.envoy.service.ext_proc.v3.ProtocolConfiguration;
45
import io.envoyproxy.envoy.service.ext_proc.v3.StreamedBodyResponse;
46
import io.grpc.CallOptions;
47
import io.grpc.Channel;
48
import io.grpc.ClientCall;
49
import io.grpc.ClientInterceptor;
50
import io.grpc.Context;
51
import io.grpc.Deadline;
52
import io.grpc.DoubleHistogramMetricInstrument;
53
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
54
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
55
import io.grpc.ManagedChannel;
56
import io.grpc.Metadata;
57
import io.grpc.MethodDescriptor;
58
import io.grpc.MetricInstrumentRegistry;
59
import io.grpc.MetricRecorder;
60
import io.grpc.Status;
61
import io.grpc.StatusRuntimeException;
62
import io.grpc.internal.DelayedClientCall;
63
import io.grpc.internal.SerializingExecutor;
64
import io.grpc.stub.ClientCallStreamObserver;
65
import io.grpc.stub.ClientResponseObserver;
66
import io.grpc.xds.ExternalProcessorFilter.ExternalProcessorFilterConfig;
67
import io.grpc.xds.Filter.FilterContext;
68
import io.grpc.xds.internal.extproc.DataPlaneCallState;
69
import io.grpc.xds.internal.extproc.EventType;
70
import io.grpc.xds.internal.extproc.ExtProcStreamState;
71
import io.grpc.xds.internal.extproc.KnownLengthInputStream;
72
import io.grpc.xds.internal.grpcservice.CachedChannelManager;
73
import io.grpc.xds.internal.grpcservice.HeaderValue;
74
import io.grpc.xds.internal.headermutations.HeaderMutationDisallowedException;
75
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
76
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
77
import io.grpc.xds.internal.headermutations.HeaderMutator;
78
import java.io.IOException;
79
import java.io.InputStream;
80
import java.util.List;
81
import java.util.Optional;
82
import java.util.Queue;
83
import java.util.concurrent.ConcurrentLinkedQueue;
84
import java.util.concurrent.Executor;
85
import java.util.concurrent.ScheduledExecutorService;
86
import java.util.concurrent.ScheduledFuture;
87
import java.util.concurrent.TimeUnit;
88
import java.util.concurrent.atomic.AtomicBoolean;
89
import java.util.concurrent.atomic.AtomicInteger;
90
import java.util.concurrent.atomic.AtomicReference;
91
import javax.annotation.Nullable;
92

93
/**
94
 * Client-side interceptor for external processing filter.
95
 */
96
final class ExternalProcessorClientInterceptor implements ClientInterceptor {
97

98
  @VisibleForTesting
99
  static DoubleHistogramMetricInstrument clientHeadersDuration;
100
  @VisibleForTesting
101
  static DoubleHistogramMetricInstrument clientHalfCloseDuration;
102
  @VisibleForTesting
103
  static DoubleHistogramMetricInstrument serverHeadersDuration;
104
  @VisibleForTesting
105
  static DoubleHistogramMetricInstrument serverTrailersDuration;
106

107
  // Copied from io.grpc.opentelemetry.internal.OpenTelemetryConstants.LATENCY_BUCKETS
108
  private static final List<Double> LATENCY_BUCKETS = ImmutableList.of(
1✔
109
      0d,     0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d,
1✔
110
      0.003d, 0.004d,   0.005d,   0.006d,  0.008d,  0.01d,   0.013d,  0.016d, 0.02d,
1✔
111
      0.025d, 0.03d,    0.04d,    0.05d,   0.065d,  0.08d,   0.1d,    0.13d,  0.16d,
1✔
112
      0.2d,   0.25d,    0.3d,     0.4d,    0.5d,    0.65d,   0.8d,    1d,     2d,
1✔
113
      5d,     10d,      20d,      50d,     100d);
1✔
114

115
  static {
116
    initMetricInstruments();
1✔
117
  }
1✔
118

119
  static synchronized void initMetricInstruments() {
120
    if (io.grpc.internal.GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
1✔
121
      if (clientHeadersDuration == null) {
1✔
122
        MetricInstrumentRegistry registry = MetricInstrumentRegistry.getDefaultRegistry();
1✔
123

124
        clientHeadersDuration = registry.registerDoubleHistogram(
1✔
125
            "grpc.client_ext_proc.client_headers_duration",
126
            "Time between when the ext_proc filter sees the client's headers and when "
127
                + "it allows those headers to continue on to the next filter",
128
            "s",
129
            LATENCY_BUCKETS,
130
            ImmutableList.of("grpc.target"),
1✔
131
            ImmutableList.of("grpc.lb.backend_service"),
1✔
132
            true);
133

134
        clientHalfCloseDuration = registry.registerDoubleHistogram(
1✔
135
            "grpc.client_ext_proc.client_half_close_duration",
136
            "Time between when the ext_proc filter sees the client's half-close and when "
137
                + "it allows that half-close to continue on to the next filter",
138
            "s",
139
            LATENCY_BUCKETS,
140
            ImmutableList.of("grpc.target"),
1✔
141
            ImmutableList.of("grpc.lb.backend_service"),
1✔
142
            true);
143

144
        serverHeadersDuration = registry.registerDoubleHistogram(
1✔
145
            "grpc.client_ext_proc.server_headers_duration",
146
            "Time between when the ext_proc filter sees the server's headers and when "
147
                + "it allows those headers to continue on to the next filter",
148
            "s",
149
            LATENCY_BUCKETS,
150
            ImmutableList.of("grpc.target"),
1✔
151
            ImmutableList.of("grpc.lb.backend_service"),
1✔
152
            true);
153

154
        serverTrailersDuration = registry.registerDoubleHistogram(
1✔
155
            "grpc.client_ext_proc.server_trailers_duration",
156
            "Time between when the ext_proc filter sees the server's trailers and when "
157
                + "it allows those trailers to continue on to the next filter",
158
            "s",
159
            LATENCY_BUCKETS,
160
            ImmutableList.of("grpc.target"),
1✔
161
            ImmutableList.of("grpc.lb.backend_service"),
1✔
162
            true);
163
      }
164
    }
165
  }
1✔
166

167
  private final ExternalProcessorFilterConfig filterConfig;
168
  private final ScheduledExecutorService scheduler;
169
  private final MetricRecorder metricsRecorder;
170
  private final ManagedChannel extProcChannel;
171

172
  @VisibleForTesting
173
  ExternalProcessorClientInterceptor(ExternalProcessorFilterConfig filterConfig,
174
      CachedChannelManager cachedChannelManager,
175
      ScheduledExecutorService scheduler,
176
      FilterContext context) {
1✔
177
    this.filterConfig = filterConfig;
1✔
178
    checkNotNull(cachedChannelManager, "cachedChannelManager");
1✔
179
    this.scheduler = checkNotNull(scheduler, "scheduler");
1✔
180
    this.metricsRecorder = checkNotNull(context.metricsRecorder(), "metricsRecorder");
1✔
181
    this.extProcChannel = cachedChannelManager.getChannel(filterConfig.getGrpcServiceConfig());
1✔
182
  }
1✔
183

184
  @VisibleForTesting
185
  ExternalProcessorFilterConfig getFilterConfig() {
186
    return filterConfig;
1✔
187
  }
188

189
  @VisibleForTesting
190
  ManagedChannel getExtProcChannel() {
191
    return extProcChannel;
×
192
  }
193

194
  @Override
195
  @SuppressWarnings("unchecked")
196
  public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
197
      MethodDescriptor<ReqT, RespT> method,
198
      CallOptions callOptions,
199
      Channel next) {
200
    SerializingExecutor serializingExecutor = new SerializingExecutor(callOptions.getExecutor());
1✔
201
    
202
    ExternalProcessorGrpc.ExternalProcessorStub extProcStub = ExternalProcessorGrpc.newStub(
1✔
203
        extProcChannel)
204
        .withExecutor(serializingExecutor);
1✔
205
    
206
    if (filterConfig.getGrpcServiceConfig().timeout().isPresent()) {
1✔
207
      long timeoutNanos = filterConfig.getGrpcServiceConfig().timeout().get().toNanos();
1✔
208
      if (timeoutNanos > 0) {
1✔
209
        extProcStub = extProcStub.withDeadlineAfter(timeoutNanos, TimeUnit.NANOSECONDS);
1✔
210
      }
211
    }
212
    if (filterConfig.getGrpcServiceConfig().initialMetadata() != null
1✔
213
        && !filterConfig.getGrpcServiceConfig().initialMetadata().isEmpty()) {
1✔
214
      Metadata extraHeaders = new Metadata();
1✔
215
      for (HeaderValue headerValue : filterConfig.getGrpcServiceConfig().initialMetadata()) {
1✔
216
        String key = headerValue.key();
1✔
217
        if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
218
          if (headerValue.rawValue().isPresent()) {
1✔
219
            Metadata.Key<byte[]> metadataKey =
1✔
220
                Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER);
1✔
221
            extraHeaders.put(metadataKey, headerValue.rawValue().get().toByteArray());
1✔
222
          }
1✔
223
        } else {
224
          if (headerValue.value().isPresent()) {
1✔
225
            Metadata.Key<String> metadataKey =
1✔
226
                Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
1✔
227
            extraHeaders.put(metadataKey, headerValue.value().get());
1✔
228
          }
229
        }
230
      }
1✔
231
      extProcStub = extProcStub.withInterceptors(
1✔
232
          io.grpc.stub.MetadataUtils.newAttachHeadersInterceptor(extraHeaders));
1✔
233
    }
234

235

236
    // The filter chain is preceded by RawMessageClientInterceptor, so ReqT and RespT are
237
    // InputStream.
238
    MethodDescriptor<InputStream, InputStream> rawMethod =
1✔
239
        (MethodDescriptor<InputStream, InputStream>) (MethodDescriptor<?, ?>) method;
240
    ClientCall<InputStream, InputStream> rawCall =
1✔
241
        (ClientCall<InputStream, InputStream>) (ClientCall<?, ?>)
242
            next.newCall(method, callOptions);
1✔
243

244
    // Create a local subclass instance to buffer outbound actions
245
    DataPlaneDelayedCall<InputStream, InputStream> delayedCall =
1✔
246
        new DataPlaneDelayedCall<>(
247
            serializingExecutor, scheduler, callOptions.getDeadline());
1✔
248

249
    DataPlaneClientCall dataPlaneCall = new DataPlaneClientCall(
1✔
250
        delayedCall, rawCall, extProcStub, filterConfig, filterConfig.getMutationRulesConfig(),
1✔
251
        scheduler, rawMethod, next, metricsRecorder, next.authority(),
1✔
252
        callOptions.getOption(XdsNameResolver.CLUSTER_SELECTION_KEY));
1✔
253

254
    return (ClientCall<ReqT, RespT>) (ClientCall<?, ?>) dataPlaneCall;
1✔
255
  }
256

257
  // --- SHARED UTILITY METHODS ---
258

259
  /**
260
   * A local subclass to expose the protected constructor of DelayedClientCall.
261
   */
262
  private static class DataPlaneDelayedCall<ReqT, RespT> extends DelayedClientCall<ReqT, RespT> {
263
    DataPlaneDelayedCall(
264
        Executor executor, ScheduledExecutorService scheduler, @Nullable Deadline deadline) {
265
      super("ext_proc", executor, scheduler, deadline);
1✔
266
    }
1✔
267
  }
268

269
  /**
270
   * Handles the bidirectional stream with the External Processor.
271
   * Buffers the actual RPC start until the Ext Proc header response is received.
272
   */
273
  private static class DataPlaneClientCall 
274
      extends SimpleForwardingClientCall<InputStream, InputStream> {
275

276
    private final ExternalProcessorGrpc.ExternalProcessorStub stub;
277
    private final ExternalProcessorFilterConfig config;
278
    private final ClientCall<InputStream, InputStream> rawCall;
279
    private final DataPlaneDelayedCall<InputStream, InputStream> delayedCall;
280
    private final ScheduledExecutorService scheduler;
281
    private final Object streamLock = new Object();
1✔
282
    @Nullable private volatile EventType expectedRequestResponse;
283
    @Nullable private volatile EventType expectedResponseResponse;
284
    @Nullable private volatile ClientCallStreamObserver<ProcessingRequest>
285
        extProcClientCallRequestObserver;
286
    private final Queue<InputStream> pendingDrainingMessages =
1✔
287
        new ConcurrentLinkedQueue<>();
288
    @Nullable private volatile DataPlaneListener wrappedListener;
289
    private final HeaderMutationFilter mutationFilter;
290
    private final HeaderMutator mutator = HeaderMutator.create();
1✔
291
    private final AtomicInteger pendingRequests = new AtomicInteger(0);
1✔
292
    private final ProcessingMode currentProcessingMode;
293
    private final MethodDescriptor<?, ?> method;
294
    private final Channel channel;
295
    private final MetricRecorder metricsRecorder;
296
    private final String target;
297
    private final String backendService;
298
    private volatile Context callContext = Context.ROOT;
1✔
299

300
    private long clientHeadersStartNanos;
301
    private long clientHalfCloseStartNanos;
302
    private long serverHeadersStartNanos;
303
    private long serverTrailersStartNanos;
304

305
    private boolean protocolConfigSent = false;
1✔
306
    private ImmutableMap<String, Struct> collectedAttributes;
307
    private boolean requestAttributesSent = false;
1✔
308
    @Nullable private volatile Metadata requestHeaders;
309
    final AtomicReference<DataPlaneCallState> dataPlaneCallState =
1✔
310
        new AtomicReference<>(DataPlaneCallState.IDLE);
311
    final AtomicReference<ExtProcStreamState> extProcStreamState =
1✔
312
        new AtomicReference<>(ExtProcStreamState.ACTIVE);
313
    final AtomicBoolean passThroughMode = new AtomicBoolean(false);
1✔
314
    final AtomicBoolean requestSideClosed = new AtomicBoolean(false);
1✔
315
    final AtomicBoolean isProcessingTrailers = new AtomicBoolean(false);
1✔
316
    final AtomicBoolean pendingHalfClose = new AtomicBoolean(false);
1✔
317
    final AtomicBoolean bodyMessageSentToExtProc = new AtomicBoolean(false);
1✔
318

319
    protected DataPlaneClientCall(
320
        DataPlaneDelayedCall<InputStream, InputStream> delayedCall,
321
        ClientCall<InputStream, InputStream> rawCall,
322
        ExternalProcessorGrpc.ExternalProcessorStub stub,
323
        ExternalProcessorFilterConfig config,
324
        Optional<HeaderMutationRulesConfig> mutationRulesConfig,
325
        ScheduledExecutorService scheduler,
326
        MethodDescriptor<?, ?> method,
327
        Channel channel,
328
        MetricRecorder metricsRecorder,
329
        String target,
330
        String backendService) {
331
      super(delayedCall);
1✔
332
      this.delayedCall = delayedCall;
1✔
333
      this.rawCall = rawCall;
1✔
334
      this.stub = stub;
1✔
335
      this.config = config;
1✔
336
      this.currentProcessingMode = config.getExternalProcessor().getProcessingMode();
1✔
337
      this.mutationFilter = new HeaderMutationFilter(mutationRulesConfig);
1✔
338
      this.scheduler = scheduler;
1✔
339
      this.method = method;
1✔
340
      this.channel = channel;
1✔
341
      this.metricsRecorder = checkNotNull(metricsRecorder, "metricsRecorder");
1✔
342
      this.target = checkNotNull(target, "target");
1✔
343
      this.backendService = checkNotNull(backendService, "backendService");
1✔
344
    }
1✔
345

346

347

348
    private void activateCall() {
349
      if ((extProcStreamState.get() == ExtProcStreamState.FAILED
1✔
350
              && !config.getFailureModeAllow()
1✔
351
              && !config.getObservabilityMode())
1✔
352
          || !dataPlaneCallState.compareAndSet(
1✔
353
              DataPlaneCallState.IDLE, DataPlaneCallState.ACTIVE)) {
354
        return;
1✔
355
      }
356
      if (clientHeadersStartNanos > 0) {
1✔
357
        long durationNanos = System.nanoTime() - clientHeadersStartNanos;
1✔
358
        recordDuration(clientHeadersDuration, durationNanos);
1✔
359
        clientHeadersStartNanos = 0;
1✔
360
      }
361
      Runnable toRun = delayedCall.setCall(rawCall);
1✔
362
      if (toRun != null) {
1✔
363
        callContext.run(toRun);
1✔
364
      }
365
      drainPendingRequests();
1✔
366
      onReadyNotify();
1✔
367
    }
1✔
368

369
    private void recordDuration(DoubleHistogramMetricInstrument instrument, long durationNanos) {
370
      if (instrument != null) {
1✔
371
        double durationSecs = (double) durationNanos / 1_000_000_000.0;
1✔
372
        metricsRecorder.recordDoubleHistogram(
1✔
373
            instrument,
374
            durationSecs,
375
            ImmutableList.of(target),
1✔
376
            ImmutableList.of(backendService));
1✔
377
      }
378
    }
1✔
379

380
    /**
381
     * Validates whether the body response uses unsupported gRPC message compression.
382
     * If compression is unsupported, this method will cancel the call, transition the
383
     * stream to a failed state, send an error to the external processor, and return false.
384
     *
385
     * @param bodyResponse the response to validate
386
     * @return true if validation passes (compression is supported or not used),
387
     *     false if validation fails
388
     */
389
    private boolean validateCompressionSupport(BodyResponse bodyResponse) {
390
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
391
        BodyMutation mutation = 
1✔
392
            bodyResponse.getResponse().getBodyMutation();
1✔
393
        if (mutation.hasStreamedResponse()
1✔
394
            && mutation.getStreamedResponse().getGrpcMessageCompressed()) {
1✔
395
          StatusRuntimeException ex = Status.UNAVAILABLE
1✔
396
              .withDescription("gRPC message compression not supported in ext_proc")
1✔
397
              .asRuntimeException();
1✔
398
          synchronized (streamLock) {
1✔
399
            if (!extProcStreamState.get().isCompleted()
1✔
400
                && extProcClientCallRequestObserver != null) {
401
              extProcClientCallRequestObserver.onError(ex);
1✔
402
            }
403
          }
1✔
404
          activateCall();
1✔
405
          markExtProcStreamFailed(extProcStreamState);
1✔
406
          delayedCall.cancel("gRPC message compression not supported in ext_proc", ex);
1✔
407
          closeExtProcStream();
1✔
408
          return false;
1✔
409
        }
410
      }
411
      return true;
1✔
412
    }
413

414

415

416
    @Override
417
    public void start(Listener<InputStream> responseListener, Metadata headers) {
418
      this.callContext = Context.current();
1✔
419
      clientHeadersStartNanos = System.nanoTime();
1✔
420
      this.requestHeaders = headers;
1✔
421
      this.wrappedListener = new DataPlaneListener(responseListener, rawCall, this);
1✔
422

423
      // DelayedClientCall.start will buffer the listener and headers until setCall is called.
424
      super.start(wrappedListener, headers);
1✔
425

426
      stub.process(new ClientResponseObserver<ProcessingRequest, ProcessingResponse>() {
1✔
427
        @Override
428
        public void beforeStart(ClientCallStreamObserver<ProcessingRequest> requestStream) {
429
          synchronized (streamLock) {
1✔
430
            extProcClientCallRequestObserver = requestStream;
1✔
431
          }
1✔
432
          requestStream.setOnReadyHandler(DataPlaneClientCall.this::onExtProcStreamReady);
1✔
433
        }
1✔
434

435
        @Override
436
        public void onNext(ProcessingResponse response) {
437
          try {
438
            if (config.getObservabilityMode()) {
1✔
439
              return;
1✔
440
            }
441

442
            if (response.hasImmediateResponse()) {
1✔
443
              if (config.getDisableImmediateResponse()) {
1✔
444
                internalOnError(Status.UNAVAILABLE
1✔
445
                    .withDescription(
1✔
446
                        "Immediate response is disabled but received from external processor")
447
                    .asRuntimeException());
1✔
448
                return;
1✔
449
              }
450
              handleImmediateResponse(response.getImmediateResponse(), wrappedListener);
1✔
451
              return;
1✔
452
            }
453

454
            if (response.hasRequestHeaders()) {
1✔
455
              EventType expected = expectedRequestResponse;
1✔
456
              if (expected == null || expected != EventType.REQUEST_HEADERS) {
1✔
457
                internalOnError(Status.UNAVAILABLE
×
458
                    .withDescription("Protocol error: received response out of order. Expected: " 
×
459
                        + expected + ", Received: REQUEST_HEADERS")
460
                    .asRuntimeException());
×
461
                return;
×
462
              }
463
              expectedRequestResponse = null;
1✔
464
            } else if (response.hasResponseHeaders()) {
1✔
465
              EventType expected = expectedResponseResponse;
1✔
466
              if (expected == null || expected != EventType.RESPONSE_HEADERS) {
1✔
467
                internalOnError(Status.UNAVAILABLE
1✔
468
                    .withDescription("Protocol error: received response out of order. Expected: " 
1✔
469
                        + expected + ", Received: RESPONSE_HEADERS")
470
                    .asRuntimeException());
1✔
471
                return;
1✔
472
              }
473
              expectedResponseResponse = null;
1✔
474
            } else if (response.hasResponseTrailers()) {
1✔
475
              EventType expected = expectedResponseResponse;
1✔
476
              if (expected == null || expected != EventType.RESPONSE_TRAILERS) {
1✔
477
                internalOnError(Status.UNAVAILABLE
1✔
478
                    .withDescription("Protocol error: received response out of order. Expected: " 
1✔
479
                        + expected + ", Received: RESPONSE_TRAILERS")
480
                    .asRuntimeException());
1✔
481
                return;
1✔
482
              }
483
              expectedResponseResponse = null;
1✔
484
            } else if (response.hasRequestBody()) {
1✔
485
              EventType expected = expectedRequestResponse;
1✔
486
              if (expected == EventType.REQUEST_HEADERS) {
1✔
487
                internalOnError(Status.UNAVAILABLE
1✔
488
                    .withDescription(
1✔
489
                        "Protocol error: received request_body before request_headers response.")
490
                    .asRuntimeException());
1✔
491
                return;
1✔
492
              }
493
            } else if (response.hasResponseBody()) {
1✔
494
              EventType expected = expectedResponseResponse;
1✔
495
              if (expected == EventType.RESPONSE_HEADERS) {
1✔
496
                internalOnError(Status.UNAVAILABLE
1✔
497
                    .withDescription(
1✔
498
                        "Protocol error: received response_body before headers response.")
499
                    .asRuntimeException());
1✔
500
                return;
1✔
501
              }
502
            }
503

504
            if (response.getRequestDrain()) {
1✔
505
              extProcStreamState.set(ExtProcStreamState.DRAINING);
1✔
506
              halfCloseExtProcStream();
1✔
507
              activateCall();
1✔
508
            }
509

510
            // 1. Client Headers
511
            if (response.hasRequestHeaders()) {
1✔
512
              if (response.getRequestHeaders().hasResponse()) {
1✔
513
                if (response.getRequestHeaders().getResponse().getStatus()
1✔
514
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
515
                  internalOnError(Status.UNAVAILABLE
1✔
516
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1✔
517
                      .asRuntimeException());
1✔
518
                  return;
1✔
519
                }
520
                applyHeaderMutations(
1✔
521
                    requestHeaders,
1✔
522
                    response.getRequestHeaders().getResponse().getHeaderMutation(),
1✔
523
                    mutationFilter,
1✔
524
                    mutator);
1✔
525
              }
526
              activateCall();
1✔
527
            }
528
            // 2. Client Message (Request Body)
529
            else if (response.hasRequestBody()) {
1✔
530
              if (validateCompressionSupport(response.getRequestBody())) {
1✔
531
                handleRequestBodyResponse(response.getRequestBody());
1✔
532
              }
533
            }
534
            // 4. Server Headers
535
            else if (response.hasResponseHeaders()) {
1✔
536
              if (response.getResponseHeaders().hasResponse()) {
1✔
537
                if (response.getResponseHeaders().getResponse().getStatus()
1✔
538
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
539
                  internalOnError(Status.UNAVAILABLE
1✔
540
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1✔
541
                      .asRuntimeException());
1✔
542
                  return;
1✔
543
                }
544
                Metadata target = wrappedListener.isTrailersOnly()
1✔
545
                    ? wrappedListener.getSavedTrailers() : wrappedListener.getSavedHeaders();
1✔
546
                applyHeaderMutations(
1✔
547
                    target,
548
                    response.getResponseHeaders().getResponse().getHeaderMutation(),
1✔
549
                    mutationFilter,
1✔
550
                    mutator);
1✔
551
              }
552
              if (wrappedListener.isTrailersOnly()) {
1✔
553
                wrappedListener.proceedWithClose();
1✔
554
              } else {
555
                wrappedListener.proceedWithHeaders();
1✔
556
              }
557
            }
558
            // 5. Server Message (Response Body)
559
            else if (response.hasResponseBody()) {
1✔
560
              if (validateCompressionSupport(response.getResponseBody())) {
1✔
561
                handleResponseBodyResponse(response.getResponseBody(), wrappedListener);
1✔
562
              }
563
            }
564
            // 6. Response Trailers
565
            else if (response.hasResponseTrailers()) {
1✔
566
              if (response.getResponseTrailers().hasHeaderMutation()) {
1✔
567
                applyHeaderMutations(
1✔
568
                    wrappedListener.getSavedTrailers(),
1✔
569
                    response.getResponseTrailers().getHeaderMutation(),
1✔
570
                    mutationFilter,
1✔
571
                    mutator);
1✔
572
              }
573
              wrappedListener.proceedWithClose();
1✔
574
            }
575

576
            checkEndOfStream(response);
1✔
577
          } catch (Throwable t) {
1✔
578
            internalOnError(t);
1✔
579
          }
1✔
580
        }
1✔
581

582
        @Override
583
        public void onError(Throwable t) {
584
          if (markExtProcStreamFailed(extProcStreamState)) {
1✔
585
            synchronized (streamLock) {
1✔
586
              extProcClientCallRequestObserver = null;
1✔
587
            }
1✔
588
            if (config.getObservabilityMode()
1✔
589
                || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1✔
590
              handleFailOpen(wrappedListener);
1✔
591
            } else {
592
              String message = "External processor stream failed";
1✔
593
              delayedCall.cancel(message, t);
1✔
594
              wrappedListener.proceedWithClose();
1✔
595
            }
596
          }
597
        }
1✔
598

599
        @Override
600
        public void onCompleted() {
601
          if (markExtProcStreamCompleted(extProcStreamState)) {
1✔
602
            handleFailOpen(wrappedListener);
1✔
603
          }
604
        }
1✔
605
      });
606

607
      this.collectedAttributes = collectAttributes(
1✔
608
          config.getRequestAttributes(), method, channel.authority(), headers);
1✔
609

610
      boolean sendRequestHeaders =
1✔
611
          currentProcessingMode.getRequestHeaderMode() == ProcessingMode.HeaderSendMode.SEND
1✔
612
          || currentProcessingMode.getRequestHeaderMode()
1✔
613
              == ProcessingMode.HeaderSendMode.DEFAULT;
614

615
      if (sendRequestHeaders) {
1✔
616
        sendToExtProc(ProcessingRequest.newBuilder()
1✔
617
            .setRequestHeaders(HttpHeaders.newBuilder()
1✔
618
                .setHeaders(toHeaderMap(headers, config.getForwardRulesConfig()))
1✔
619
                .setEndOfStream(false)
1✔
620
                .build())
1✔
621
            .build());
1✔
622
      }
623

624
      if (config.getObservabilityMode() || !sendRequestHeaders) {
1✔
625
        activateCall();
1✔
626
      }
627
    }
1✔
628

629
    private void sendToExtProc(ProcessingRequest request) {
630
      synchronized (streamLock) {
1✔
631
        if (extProcStreamState.get().isCompleted()) {
1✔
632
          return;
×
633
        }
634
        
635
        if (request.hasRequestHeaders()) {
1✔
636
          expectedRequestResponse = EventType.REQUEST_HEADERS;
1✔
637
        } else if (request.hasResponseHeaders()) {
1✔
638
          expectedResponseResponse = EventType.RESPONSE_HEADERS;
1✔
639
        } else if (request.hasResponseTrailers()) {
1✔
640
          expectedResponseResponse = EventType.RESPONSE_TRAILERS;
1✔
641
        }
642

643
        ProcessingRequest requestToSend = request;
1✔
644
        if (!protocolConfigSent) {
1✔
645
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
646
              .setProtocolConfig(ProtocolConfiguration.newBuilder()
1✔
647
                  .setRequestBodyMode(currentProcessingMode.getRequestBodyMode())
1✔
648
                  .setResponseBodyMode(currentProcessingMode.getResponseBodyMode())
1✔
649
                  .build())
1✔
650
              .build();
1✔
651
          protocolConfigSent = true;
1✔
652
        }
653

654
        boolean isClientServerMessage =
1✔
655
            requestToSend.hasRequestHeaders() || requestToSend.hasRequestBody();
1✔
656
        if (isClientServerMessage
1✔
657
            && !requestAttributesSent
658
            && collectedAttributes != null
659
            && !collectedAttributes.isEmpty()) {
1✔
660
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
661
              .putAllAttributes(collectedAttributes)
1✔
662
              .build();
1✔
663
          requestAttributesSent = true;
1✔
664
        }
665

666
        if (config.getObservabilityMode()) {
1✔
667
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
668
              .setObservabilityMode(true)
1✔
669
              .build();
1✔
670
        }
671

672
        extProcClientCallRequestObserver.onNext(requestToSend);
1✔
673
      }
1✔
674
    }
1✔
675

676
    private void onExtProcStreamReady() {
677
      drainPendingRequests();
1✔
678
      onReadyNotify();
1✔
679
    }
1✔
680

681
    private void drainPendingRequests() {
682
      int toRequest = pendingRequests.getAndSet(0);
1✔
683
      if (toRequest > 0) {
1✔
684
        super.request(toRequest);
1✔
685
      }
686
    }
1✔
687

688
    private void closeExtProcStream() {
689
      synchronized (streamLock) {
1✔
690
        if (markExtProcStreamCompleted(extProcStreamState)) {
1✔
691
          if (extProcClientCallRequestObserver != null) {
1✔
692
            extProcClientCallRequestObserver.onCompleted();
1✔
693
          }
694
        }
695
      }
1✔
696
    }
1✔
697

698
    private void internalOnError(Throwable t) {
699
      if (markExtProcStreamFailed(extProcStreamState)) {
1✔
700
        synchronized (streamLock) {
1✔
701
          if (extProcClientCallRequestObserver != null) {
1✔
702
            try {
703
              extProcClientCallRequestObserver.onError(t);
1✔
704
            } catch (Throwable ignored) {
×
705
              // Ignore exceptions during cancel/onError propagation
706
            }
1✔
707
            extProcClientCallRequestObserver = null;
1✔
708
          }
709
        }
1✔
710
        if (config.getObservabilityMode()
1✔
711
            || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1✔
712
          handleFailOpen(wrappedListener);
1✔
713
        } else {
714
          String message = "External processor stream failed";
1✔
715
          delayedCall.cancel(message, t);
1✔
716
          wrappedListener.proceedWithClose();
1✔
717
        }
718
      }
719
    }
1✔
720

721
    private void halfCloseExtProcStream() {
722
      synchronized (streamLock) {
1✔
723
        if (!extProcStreamState.get().isCompleted() && extProcClientCallRequestObserver != null) {
1✔
724
          extProcClientCallRequestObserver.onCompleted();
1✔
725
        }
726
      }
1✔
727
    }
1✔
728

729
    private void onReadyNotify() {
730
      wrappedListener.onReadyNotify();
1✔
731
    }
1✔
732

733
    private boolean isSidecarReady() {
734
      ExtProcStreamState state = extProcStreamState.get();
1✔
735
      if (state.isCompleted()) {
1✔
736
        return true;
×
737
      }
738
      if (state.isDraining()) {
1✔
739
        return false;
1✔
740
      }
741
      synchronized (streamLock) {
1✔
742
        ClientCallStreamObserver<ProcessingRequest> observer = extProcClientCallRequestObserver;
1✔
743
        return observer != null && observer.isReady();
1✔
744
      }
745
    }
746

747
    @Override
748
    public boolean isReady() {
749
      if (passThroughMode.get()) {
1✔
750
        return super.isReady();
1✔
751
      }
752
      if (extProcStreamState.get().isCompleted()) {
1✔
753
        return super.isReady();
×
754
      }
755
      if (dataPlaneCallState.get() == DataPlaneCallState.IDLE && !config.getObservabilityMode()) {
1✔
756
        return false;
1✔
757
      }
758
      boolean sidecarReady = isSidecarReady();
1✔
759
      if (config.getObservabilityMode()) {
1✔
760
        return super.isReady() && sidecarReady;
1✔
761
      }
762
      return sidecarReady;
1✔
763
    }
764

765
    @Override
766
    public void request(int numMessages) {
767
      if (passThroughMode.get() || extProcStreamState.get().isCompleted()) {
1✔
768
        super.request(numMessages);
1✔
769
        return;
1✔
770
      }
771
      if (!isSidecarReady()) {
1✔
772
        pendingRequests.addAndGet(numMessages);
1✔
773
        return;
1✔
774
      }
775
      super.request(numMessages);
1✔
776
    }
1✔
777

778
    @Override
779
    public void sendMessage(InputStream message) {
780
      if (requestSideClosed.get()) {
1✔
781
        // External processor already closed the request stream. Discard further messages.
782
        return;
1✔
783
      }
784

785
      if (passThroughMode.get()) {
1✔
786
        super.sendMessage(message);
1✔
787
        return;
1✔
788
      }
789

790
      synchronized (streamLock) {
1✔
791
        if (passThroughMode.get()) {
1✔
792
          super.sendMessage(message);
×
793
          return;
×
794
        }
795

796
        ExtProcStreamState state = extProcStreamState.get();
1✔
797
        if (state.isDraining() || state.isCompleted()) {
1✔
798
          try {
799
            ByteString copiedBody = ByteString.readFrom(message);
1✔
800
            pendingDrainingMessages.add(new KnownLengthInputStream(copiedBody));
1✔
801
          } catch (IOException e) {
×
802
            rawCall.cancel("Failed to copy outbound message for buffering", e);
×
803
          }
1✔
804
          return;
1✔
805
        }
806
      }
1✔
807

808
      if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1✔
809
        super.sendMessage(message);
1✔
810
        return;
1✔
811
      }
812

813
      // Mode is GRPC
814
      try {
815
        ByteString bodyByteString = outboundStreamToByteString(message);
1✔
816
        sendToExtProc(ProcessingRequest.newBuilder()
1✔
817
            .setRequestBody(HttpBody.newBuilder()
1✔
818
                .setBody(bodyByteString)
1✔
819
                .setEndOfStream(false)
1✔
820
                .build())
1✔
821
            .build());
1✔
822
        bodyMessageSentToExtProc.set(true);
1✔
823

824
        if (config.getObservabilityMode()) {
1✔
825
          super.sendMessage(new KnownLengthInputStream(bodyByteString));
×
826
        }
827
      } catch (IOException e) {
×
828
        rawCall.cancel("Failed to serialize message for External Processor", e);
×
829
      }
1✔
830
    }
1✔
831

832
    private void proceedWithHalfClose() {
833
      if (clientHalfCloseStartNanos > 0) {
1✔
834
        long durationNanos = System.nanoTime() - clientHalfCloseStartNanos;
1✔
835
        recordDuration(clientHalfCloseDuration, durationNanos);
1✔
836
        clientHalfCloseStartNanos = 0;
1✔
837
      }
838
      super.halfClose();
1✔
839
    }
1✔
840

841
    @Override
842
    public void halfClose() {
843
      clientHalfCloseStartNanos = System.nanoTime();
1✔
844
      if (passThroughMode.get() || extProcStreamState.get().isCompleted()) {
1✔
845
        if (requestSideClosed.compareAndSet(false, true)) {
1✔
846
          proceedWithHalfClose();
1✔
847
        }
848
        return;
1✔
849
      }
850

851
      pendingHalfClose.set(true);
1✔
852

853
      if (extProcStreamState.get().isDraining()) {
1✔
854
        return;
1✔
855
      }
856

857
      if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1✔
858
        if (requestSideClosed.compareAndSet(false, true)) {
1✔
859
          proceedWithHalfClose();
1✔
860
        }
861
        return;
1✔
862
      }
863

864
      // Mode is GRPC
865
      sendToExtProc(ProcessingRequest.newBuilder()
1✔
866
          .setRequestBody(HttpBody.newBuilder()
1✔
867
              .setEndOfStreamWithoutMessage(true)
1✔
868
              .build())
1✔
869
          .build());
1✔
870
      
871
      // Defer super.halfClose() until ext-proc response signals end_of_stream.
872
    }
1✔
873

874
    @Override
875
    public void cancel(@Nullable String message, @Nullable Throwable cause) {
876
      synchronized (streamLock) {
1✔
877
        if (!extProcStreamState.get().isCompleted() && extProcClientCallRequestObserver != null) {
1✔
878
          extProcClientCallRequestObserver.onError(
1✔
879
              Status.CANCELLED
880
                  .withDescription(message)
1✔
881
                  .withCause(cause)
1✔
882
                  .asRuntimeException());
1✔
883
        }
884
      }
1✔
885
      super.cancel(message, cause);
1✔
886
    }
1✔
887

888
    private void handleRequestBodyResponse(BodyResponse bodyResponse) {
889
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
890
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1✔
891
        if (mutation.hasStreamedResponse()) {
1✔
892
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1✔
893
          if (!streamed.getEndOfStreamWithoutMessage()) {
1✔
894
            super.sendMessage(new KnownLengthInputStream(streamed.getBody()));
1✔
895
          }
896
          if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) {
1✔
897
            if (requestSideClosed.compareAndSet(false, true)) {
1✔
898
              proceedWithHalfClose();
1✔
899
            }
900
          }
901
        }
902
      }
903
    }
1✔
904

905
    private void handleResponseBodyResponse(
906
        BodyResponse bodyResponse, DataPlaneListener listener) {
907
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
908
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1✔
909
        if (mutation.hasStreamedResponse()) {
1✔
910
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1✔
911
          listener.onExternalBody(streamed.getBody());
1✔
912
        }
913
      }
914
    }
1✔
915

916
    private void handleImmediateResponse(ImmediateResponse immediate, DataPlaneListener listener)
917
        throws HeaderMutationDisallowedException {
918
      Status status = Status.fromCodeValue(immediate.getGrpcStatus().getStatus());
1✔
919
      if (!immediate.getDetails().isEmpty()) {
1✔
920
        status = status.withDescription(immediate.getDetails());
1✔
921
      }
922

923
      Metadata trailers = new Metadata();
1✔
924
      if (immediate.hasHeaders()) {
1✔
925
        applyHeaderMutations(trailers, immediate.getHeaders(), mutationFilter, mutator);
1✔
926
      }
927

928
      listener.setImmediateResponse(status, trailers);
1✔
929

930
      if (isProcessingTrailers.get()) {
1✔
931
        // If sent in response to a server trailers event, sets the status and optionally
932
        // headers to be included in the trailers.
933
        listener.unblockAfterStreamComplete();
1✔
934
      } else {
935
        // If sent in response to any other event, it will cause the data plane RPC to
936
        // immediately fail with the specified status as if it were an out-of-band
937
        // cancellation.
938
        rawCall.cancel(status.getDescription(), null);
1✔
939
        listener.unblockAfterStreamComplete();
1✔
940
      }
941
      closeExtProcStream();
1✔
942
    }
1✔
943

944
    private void drainPendingDrainingMessages() {
945
      synchronized (streamLock) {
1✔
946
        InputStream msg;
947
        while ((msg = pendingDrainingMessages.poll()) != null) {
1✔
948
          super.sendMessage(msg);
1✔
949
        }
950
        passThroughMode.set(true);
1✔
951
        if (pendingHalfClose.get()) {
1✔
952
          if (requestSideClosed.compareAndSet(false, true)) {
1✔
953
            proceedWithHalfClose();
1✔
954
          }
955
        }
956
      }
1✔
957
    }
1✔
958

959
    private void handleFailOpen(DataPlaneListener listener) {
960
      activateCall();
1✔
961
      drainPendingRequests();
1✔
962
      listener.unblockAfterStreamComplete();
1✔
963
      closeExtProcStream();
1✔
964
    }
1✔
965

966
    private void checkEndOfStream(ProcessingResponse response) {
967
      boolean terminal = false;
1✔
968
      if (response.hasResponseTrailers()) {
1✔
969
        terminal = true;
1✔
970
      } else if (response.hasResponseHeaders() && wrappedListener.isTrailersOnly()) {
1✔
971
        terminal = true;
1✔
972
      }
973

974
      if (terminal) {
1✔
975
        wrappedListener.unblockAfterStreamComplete();
1✔
976
        closeExtProcStream();
1✔
977
      }
978
    }
1✔
979

980
    long getServerHeadersStartNanos() {
981
      return serverHeadersStartNanos;
1✔
982
    }
983

984
    void setServerHeadersStartNanos(long serverHeadersStartNanos) {
985
      this.serverHeadersStartNanos = serverHeadersStartNanos;
1✔
986
    }
1✔
987

988
    long getServerTrailersStartNanos() {
989
      return serverTrailersStartNanos;
1✔
990
    }
991

992
    void setServerTrailersStartNanos(long serverTrailersStartNanos) {
993
      this.serverTrailersStartNanos = serverTrailersStartNanos;
1✔
994
    }
1✔
995

996
    AtomicReference<ExtProcStreamState> getExtProcStreamState() {
997
      return extProcStreamState;
1✔
998
    }
999

1000
    ProcessingMode getCurrentProcessingMode() {
1001
      return currentProcessingMode;
1✔
1002
    }
1003

1004
    AtomicBoolean getPassThroughMode() {
1005
      return passThroughMode;
1✔
1006
    }
1007

1008
    ExternalProcessorFilterConfig getConfig() {
1009
      return config;
1✔
1010
    }
1011

1012
    Context getCallContext() {
1013
      return callContext;
1✔
1014
    }
1015

1016
    ScheduledExecutorService getScheduler() {
1017
      return scheduler;
1✔
1018
    }
1019

1020
    AtomicBoolean getIsProcessingTrailers() {
1021
      return isProcessingTrailers;
1✔
1022
    }
1023
  }
1024

1025
  private static class DataPlaneListener extends SimpleForwardingClientCallListener<InputStream> {
1026
    private final ClientCall<?, ?> rawCall;
1027
    private final DataPlaneClientCall dataPlaneClientCall;
1028
    private final Queue<InputStream> savedMessages = new ConcurrentLinkedQueue<>();
1✔
1029
    private boolean inboundPassThrough = false;
1✔
1030
    @Nullable private volatile Metadata savedHeaders;
1031
    @Nullable private volatile Metadata savedTrailers;
1032
    @Nullable private volatile Status savedStatus;
1033
    private final AtomicBoolean terminationTriggered = new AtomicBoolean(false);
1✔
1034
    private final AtomicBoolean responseHeadersSent = new AtomicBoolean(false);
1✔
1035
    private final AtomicBoolean trailersOnly = new AtomicBoolean(false);
1✔
1036

1037
    protected DataPlaneListener(
1038
        ClientCall.Listener<InputStream> delegate,
1039
        ClientCall<?, ?> rawCall,
1040
        DataPlaneClientCall dataPlaneClientCall) {
1041
      super(delegate);
1✔
1042
      this.rawCall = rawCall;
1✔
1043
      this.dataPlaneClientCall = dataPlaneClientCall;
1✔
1044
    }
1✔
1045

1046
    boolean isTrailersOnly() {
1047
      return trailersOnly.get();
1✔
1048
    }
1049

1050
    Metadata getSavedHeaders() {
1051
      return savedHeaders;
1✔
1052
    }
1053

1054
    Metadata getSavedTrailers() {
1055
      return savedTrailers;
1✔
1056
    }
1057

1058
    void setImmediateResponse(Status status, Metadata trailers) {
1059
      this.savedStatus = status;
1✔
1060
      this.savedTrailers = trailers;
1✔
1061
    }
1✔
1062

1063
    @Override
1064
    public void onReady() {
1065
      dataPlaneClientCall.drainPendingRequests();
1✔
1066
      onReadyNotify();
1✔
1067
    }
1✔
1068

1069
    @Override
1070
    public void onHeaders(Metadata headers) {
1071
      dataPlaneClientCall.setServerHeadersStartNanos(System.nanoTime());
1✔
1072
      responseHeadersSent.set(true);
1✔
1073
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1074
        this.savedHeaders = headers;
1✔
1075
        return;
1✔
1076
      }
1077
      boolean sendResponseHeaders =
1✔
1078
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1079
              == ProcessingMode.HeaderSendMode.SEND
1080
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1081
              == ProcessingMode.HeaderSendMode.DEFAULT;
1082

1083
      if (dataPlaneClientCall.getPassThroughMode().get() 
1✔
1084
          || dataPlaneClientCall.getExtProcStreamState().get().isCompleted() 
1✔
1085
          || !sendResponseHeaders) {
1086
        proceedWithHeaders(headers);
1✔
1087
        return;
1✔
1088
      }
1089

1090
      this.savedHeaders = headers;
1✔
1091
      dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1092
          .setResponseHeaders(HttpHeaders.newBuilder()
1✔
1093
              .setHeaders(
1✔
1094
                  toHeaderMap(headers, dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1095
              .build())
1✔
1096
          .build());
1✔
1097

1098
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1099
        proceedWithHeaders();
×
1100
      }
1101
    }
1✔
1102

1103
    @Override
1104
    public void onMessage(InputStream message) {
1105
      synchronized (savedMessages) {
1✔
1106
        if (inboundPassThrough) {
1✔
1107
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1✔
1108
          return;
1✔
1109
        }
1110

1111
        if (savedHeaders != null
1✔
1112
            || dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1113
          try {
1114
            ByteString copiedBody = ByteString.readFrom(message);
1✔
1115
            savedMessages.add(new KnownLengthInputStream(copiedBody));
1✔
1116
          } catch (IOException e) {
×
1117
            rawCall.cancel("Failed to copy inbound message for buffering", e);
×
1118
          }
1✔
1119
          return;
1✔
1120
        }
1121
      }
1✔
1122

1123
      if (dataPlaneClientCall.getPassThroughMode().get()) {
1✔
1124
        dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
×
1125
        return;
×
1126
      }
1127

1128
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1129
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1✔
1130
              != ProcessingMode.BodySendMode.GRPC) {
1131
        dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1✔
1132
        return;
1✔
1133
      }
1134

1135
      try {
1136
        ByteString bodyByteString = ByteString.readFrom(message);
1✔
1137
        sendResponseBodyToExtProc(bodyByteString, false);
1✔
1138
        dataPlaneClientCall.bodyMessageSentToExtProc.set(true);
1✔
1139

1140
        if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1141
          // If needed, downstream reading can be made more optimal by creating a wrapped
1142
          // Inputstream wraps the underlying bytestring and that implements HasByteBuffer,
1143
          // Detachable, KnownLength
1144
          dataPlaneClientCall.getCallContext().run(
×
1145
              () -> delegate().onMessage(bodyByteString.newInput()));
×
1146
        }
1147
      } catch (IOException e) {
×
1148
        rawCall.cancel("Failed to read server response", e);
×
1149
      }
1✔
1150
    }
1✔
1151

1152
    @Override
1153
    public void onClose(Status status, Metadata trailers) {
1154
      dataPlaneClientCall.setServerTrailersStartNanos(System.nanoTime());
1✔
1155
      ExtProcStreamState extProcStreamState =
1✔
1156
          dataPlaneClientCall.getExtProcStreamState().get();
1✔
1157
      if (extProcStreamState.isFailed()
1✔
1158
          && !dataPlaneClientCall.getConfig().getObservabilityMode()
1✔
1159
          && (!dataPlaneClientCall.getConfig().getFailureModeAllow()
1✔
1160
              || dataPlaneClientCall.bodyMessageSentToExtProc.get())) {
1✔
1161
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1162
          proceedWithClose(Status.INTERNAL.withDescription("External processor stream failed")
1✔
1163
              .withCause(status.getCause()), new Metadata());
1✔
1164
        }
1165
        return;
1✔
1166
      }
1167
      if (dataPlaneClientCall.getPassThroughMode().get()) {
1✔
1168
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1169
          proceedWithClose(status, trailers);
1✔
1170
        }
1171
        return;
1✔
1172
      }
1173

1174
      if (this.savedStatus == null) {
1✔
1175
        this.savedStatus = status;
1✔
1176
        this.savedTrailers = trailers;
1✔
1177
      }
1178

1179
      // If we are still waiting for the external processor to validate response headers,
1180
      // buffer the close status/trailers and defer the close until headers are processed.
1181
      if (savedHeaders != null) {
1✔
1182
        return;
1✔
1183
      }
1184

1185
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1186
        return;
1✔
1187
      }
1188

1189
      if (!responseHeadersSent.get()) {
1✔
1190
        trailersOnly.set(true);
1✔
1191
      }
1192

1193
      triggerCloseHandshake();
1✔
1194

1195
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1196
        proceedWithClose();
1✔
1197
        @SuppressWarnings("unused")
1198
        ScheduledFuture<?> unused = dataPlaneClientCall.getScheduler().schedule(
1✔
1199
            dataPlaneClientCall::closeExtProcStream,
1✔
1200
            dataPlaneClientCall.getConfig().getDeferredCloseTimeoutNanos(),
1✔
1201
            TimeUnit.NANOSECONDS);
1202
      }
1203
    }
1✔
1204

1205
    void onReadyNotify() {
1206
      dataPlaneClientCall.getCallContext().run(() -> delegate().onReady());
1✔
1207
    }
1✔
1208

1209
    void proceedWithHeaders() {
1210
      if (savedHeaders != null) {
1✔
1211
        proceedWithHeaders(savedHeaders);
1✔
1212
        synchronized (savedMessages) {
1✔
1213
          savedHeaders = null;
1✔
1214
          if (!dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1215
            InputStream msg;
1216
            while ((msg = savedMessages.poll()) != null) {
1✔
1217
              onMessage(msg);
1✔
1218
            }
1219
          }
1220
        }
1✔
1221
        onReadyNotify();
1✔
1222
        if (savedStatus != null) {
1✔
1223
          triggerCloseHandshake();
1✔
1224
        }
1225
      }
1226
    }
1✔
1227

1228
    private void proceedWithHeaders(Metadata headers) {
1229
      if (dataPlaneClientCall.getServerHeadersStartNanos() > 0) {
1✔
1230
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerHeadersStartNanos();
1✔
1231
        dataPlaneClientCall.recordDuration(serverHeadersDuration, durationNanos);
1✔
1232
        dataPlaneClientCall.setServerHeadersStartNanos(0);
1✔
1233
      }
1234
      dataPlaneClientCall.getCallContext().run(() -> delegate().onHeaders(headers));
1✔
1235
    }
1✔
1236

1237
    void proceedWithClose() {
1238
      if (savedStatus != null) {
1✔
1239
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1240
          proceedWithClose(savedStatus, savedTrailers);
1✔
1241
        }
1242
        savedStatus = null;
1✔
1243
        savedTrailers = null;
1✔
1244
      }
1245
    }
1✔
1246

1247
    private void proceedWithClose(Status status, Metadata trailers) {
1248
      if (dataPlaneClientCall.getServerTrailersStartNanos() > 0) {
1✔
1249
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerTrailersStartNanos();
1✔
1250
        dataPlaneClientCall.recordDuration(serverTrailersDuration, durationNanos);
1✔
1251
        dataPlaneClientCall.setServerTrailersStartNanos(0);
1✔
1252
      }
1253
      dataPlaneClientCall.getCallContext().run(() -> delegate().onClose(status, trailers));
1✔
1254
    }
1✔
1255

1256
    void onExternalBody(ByteString body) {
1257
      // If needed, downstream reading can be made more optimal by creating a wrapped
1258
      // Inputstream wraps the underlying bytestring and that implements HasByteBuffer,
1259
      // Detachable, KnownLength
1260
      dataPlaneClientCall.getCallContext().run(
1✔
1261
          () -> delegate().onMessage(body.newInput()));
1✔
1262
    }
1✔
1263

1264
    void unblockAfterStreamComplete() {
1265
      proceedWithHeaders();
1✔
1266
      proceedWithSavedMessages();
1✔
1267
      dataPlaneClientCall.drainPendingDrainingMessages();
1✔
1268
      proceedWithClose();
1✔
1269
    }
1✔
1270

1271
    private void proceedWithSavedMessages() {
1272
      synchronized (savedMessages) {
1✔
1273
        InputStream msg;
1274
        while ((msg = savedMessages.poll()) != null) {
1✔
1275
          final InputStream finalMsg = msg;
1✔
1276
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(finalMsg));
1✔
1277
        }
1✔
1278
        inboundPassThrough = true;
1✔
1279
      }
1✔
1280
    }
1✔
1281

1282
    private void triggerCloseHandshake() {
1283
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1284
          || !terminationTriggered.compareAndSet(false, true)) {
1✔
1285
        return;
1✔
1286
      }
1287

1288
      boolean sendResponseHeaders =
1✔
1289
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1290
              == ProcessingMode.HeaderSendMode.SEND
1291
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1292
              == ProcessingMode.HeaderSendMode.DEFAULT;
1293

1294
      boolean sendResponseTrailers =
1✔
1295
          dataPlaneClientCall.getCurrentProcessingMode().getResponseTrailerMode()
1✔
1296
              == ProcessingMode.HeaderSendMode.SEND;
1297

1298
      if (trailersOnly.get()) {
1✔
1299
        if (sendResponseHeaders) {
1✔
1300
          dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1301
              .setResponseHeaders(HttpHeaders.newBuilder()
1✔
1302
                  .setHeaders(
1✔
1303
                      toHeaderMap(
1✔
1304
                          savedTrailers,
1305
                          dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1306
                  .setEndOfStream(true)
1✔
1307
                  .build())
1✔
1308
              .build());
1✔
1309
        } else {
1310
          proceedWithClose();
1✔
1311
          if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1312
            dataPlaneClientCall.closeExtProcStream();
1✔
1313
          }
1314
        }
1315
      } else if (sendResponseTrailers) {
1✔
1316
        dataPlaneClientCall.getIsProcessingTrailers().set(true);
1✔
1317
        dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1318
            .setResponseTrailers(HttpTrailers.newBuilder()
1✔
1319
                .setTrailers(
1✔
1320
                    toHeaderMap(
1✔
1321
                        savedTrailers,
1322
                        dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1323
                .build())
1✔
1324
            .build());
1✔
1325
      } else {
1326
        proceedWithClose();
1✔
1327
        if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1328
          dataPlaneClientCall.closeExtProcStream();
1✔
1329
        }
1330
      }
1331
    }
1✔
1332

1333
    private void sendResponseBodyToExtProc(
1334
        @Nullable ByteString bodyByteString, boolean endOfStream) {
1335
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1336
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1✔
1337
              != ProcessingMode.BodySendMode.GRPC) {
1338
        return;
×
1339
      }
1340

1341
      HttpBody.Builder bodyBuilder =
1342
          HttpBody.newBuilder();
1✔
1343
      if (bodyByteString != null) {
1✔
1344
        bodyBuilder.setBody(bodyByteString);
1✔
1345
      }
1346
      bodyBuilder.setEndOfStream(endOfStream);
1✔
1347

1348
      dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1349
          .setResponseBody(bodyBuilder.build())
1✔
1350
          .build());
1✔
1351
    }
1✔
1352
  }
1353
}
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