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

grpc / grpc-java / #20330

22 Jun 2026 09:42AM UTC coverage: 88.863% (-0.01%) from 88.874%
#20330

push

github

web-flow
xds: ignore keep_empty_value and discard header keys on empty mutations (#12852)

In GrpcService/ext_proc header mutations, we do not support the
`keep_empty_value` field
(https://github.com/grpc/proposal/pull/510/changes/f6d42d67d).
If any header mutation results in a header containing an empty value
(either string or binary), we proceed with the empty value.

36522 of 41099 relevant lines covered (88.86%)

0.89 hits per line

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

97.62
/../xds/src/main/java/io/grpc/xds/internal/headermutations/HeaderMutator.java
1
/*
2
 * Copyright 2025 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.internal.headermutations;
18

19

20
import io.grpc.Metadata;
21
import io.grpc.xds.internal.grpcservice.HeaderValue;
22
import io.grpc.xds.internal.headermutations.HeaderValueOption.HeaderAppendAction;
23
import java.util.logging.Logger;
24

25
/**
26
 * The HeaderMutator provides methods to apply header mutations to a given set of headers based on a
27
 * given set of rules.
28
 */
29
public class HeaderMutator {
30

31
  private static final Logger logger = Logger.getLogger(HeaderMutator.class.getName());
1✔
32

33
  /**
34
   * Creates a new instance of {@code HeaderMutator}.
35
   */
36
  public static HeaderMutator create() {
37
    return new HeaderMutator();
1✔
38
  }
39

40
  HeaderMutator() {}
1✔
41

42
  /**
43
   * Applies the given header mutations to the provided metadata headers.
44
   *
45
   * @param mutations The header mutations to apply.
46
   * @param headers The metadata headers to which the mutations will be applied.
47
   */
48
  public void applyMutations(final HeaderMutations mutations, Metadata headers) {
49
    // TODO(sauravzg): The specification is not clear on order of header removals and additions.
50
    // in case of conflicts. Copying the order from Envoy here, which does removals at the end.
51
    applyHeaderUpdates(mutations.headers(), headers);
1✔
52
    for (String headerToRemove : mutations.headersToRemove()) {
1✔
53
      Metadata.Key<?> key = headerToRemove.endsWith(Metadata.BINARY_HEADER_SUFFIX)
1✔
54
          ? Metadata.Key.of(headerToRemove, Metadata.BINARY_BYTE_MARSHALLER)
1✔
55
          : Metadata.Key.of(headerToRemove, Metadata.ASCII_STRING_MARSHALLER);
1✔
56
      headers.discardAll(key);
1✔
57
    }
1✔
58
  }
1✔
59

60
  private void applyHeaderUpdates(final Iterable<HeaderValueOption> headerOptions,
61
      Metadata headers) {
62
    for (HeaderValueOption headerOption : headerOptions) {
1✔
63
      updateHeader(headerOption, headers);
1✔
64
    }
1✔
65
  }
1✔
66

67
  private void updateHeader(final HeaderValueOption option, Metadata mutableHeaders) {
68
    HeaderValue header = option.header();
1✔
69
    HeaderAppendAction action = option.appendAction();
1✔
70

71
    if (header.key().endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
72
      if (header.rawValue().isPresent()) {
1✔
73
        Metadata.Key<byte[]> key = Metadata.Key.of(header.key(), Metadata.BINARY_BYTE_MARSHALLER);
1✔
74
        updateHeader(action, key, header.rawValue().get().toByteArray(), mutableHeaders);
1✔
75
      } else {
1✔
76
        logger.fine("Missing binary rawValue for header: " + header.key());
1✔
77
      }
78
    } else {
79
      if (header.value().isPresent()) {
1✔
80
        Metadata.Key<String> key = Metadata.Key.of(header.key(), Metadata.ASCII_STRING_MARSHALLER);
1✔
81
        updateHeader(action, key, header.value().get(), mutableHeaders);
1✔
82
      } else {
1✔
83
        logger.fine("Missing value for header: " + header.key());
1✔
84
      }
85
    }
86
  }
1✔
87

88
  private <T> void updateHeader(final HeaderAppendAction action, final Metadata.Key<T> key,
89
      final T value, Metadata mutableHeaders) {
90
    switch (action) {
1✔
91
      case APPEND_IF_EXISTS_OR_ADD:
92
        mutableHeaders.put(key, value);
1✔
93
        break;
1✔
94
      case ADD_IF_ABSENT:
95
        if (!mutableHeaders.containsKey(key)) {
1✔
96
          mutableHeaders.put(key, value);
1✔
97
        }
98
        break;
99
      case OVERWRITE_IF_EXISTS_OR_ADD:
100
        mutableHeaders.discardAll(key);
1✔
101
        mutableHeaders.put(key, value);
1✔
102
        break;
1✔
103
      case OVERWRITE_IF_EXISTS:
104
        if (mutableHeaders.containsKey(key)) {
1✔
105
          mutableHeaders.discardAll(key);
1✔
106
          mutableHeaders.put(key, value);
1✔
107
        }
108
        break;
109

110
      default:
111
        // Should be unreachable unless there's a proto schema mismatch.
112
        logger.fine("Unknown HeaderAppendAction: " + action);
×
113
    }
114
  }
1✔
115
}
116

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